# @summary Manages the Apt sources in /etc/apt/sources.list.d/.
#
# @example Install the puppetlabs apt source
#   apt::source { 'puppetlabs':
#     location => 'http://apt.puppetlabs.com',
#     repos    => 'main',
#     key      => {
#       id     => '6F6B15509CF8E59E6E469F327F438280EF8D349F',
#       server => 'keyserver.ubuntu.com',
#     },
#   }
#
# @example Download key behaviour to handle modern apt gpg keyrings. The `name` parameter in the key hash should be given with
#   extension. Absence of extension will result in file formation with just name and no extension.
#   apt::source { 'puppetlabs':
#     location => 'http://apt.puppetlabs.com',
#     comment  => 'Puppet8',
#     key      => {
#       'name'   => 'puppetlabs.gpg',
#       'source' => 'https://apt.puppetlabs.com/keyring.gpg',
#     },
#   }
#
# @example Install the puppetlabs apt source (deb822 format)
#   apt::source { 'puppetlabs':
#     source_format => 'sources'
#     location      => ['http://apt.puppetlabs.com'],
#     repos         => ['puppet8'],
#     keyring       => '/etc/apt/keyrings/puppetlabs.gpg',
#   }
#
# @param source_format
#   The file format to use for the apt source. See https://wiki.debian.org/SourcesList
#
# @param location
#   Required, unless ensure is set to 'absent'. Specifies an Apt repository. Valid options: a string containing a repository URL.
#   DEB822: Supports an array of URL values
#
# @param types
#   DEB822: The package types this source manages.
#
# @param enabled
#   DEB822: Enable or Disable the APT source.
#
# @param comment
#   Supplies a comment for adding to the Apt source file.
#
# @param ensure
#   Specifies whether the Apt source file should exist.
#
# @param release
#   Specifies a distribution of the Apt repository.
#   DEB822: Supports an array of values
#
# @param repos
#   Specifies a component of the Apt repository.
#   DEB822: Supports an array of values
#
# @param include
#   Configures include options. Valid options: a hash of available keys.
#
# @option include [Boolean] :deb
#   Specifies whether to request the distribution's compiled binaries.
#
# @option include [Boolean] :src
#   Specifies whether to request the distribution's uncompiled source code.
#
# @param key
#   Creates an `apt::keyring` in `/etc/apt/keyrings` (or anywhere on disk given `filename`) Valid options:
#     * a hash of `parameter => value` pairs to be passed to `file`: `name` (title), `content`, `source`, `filename`
#
#   The following inputs are valid for the (deprecated) `apt::key` defined type. Valid options:
#     * a string to be passed to the `id` parameter of the `apt::key` defined type
#     * a hash of `parameter => value` pairs to be passed to `apt::key`: `id`, `server`, `content`, `source`, `weak_ssl`, `options`
#
# @param keyring
#   Absolute path to a file containing the PGP keyring used to sign this repository. Value is used to set signed-by on the source entry.
#   This is not necessary if the key is installed with `key` param above.
#   See https://wiki.debian.org/DebianRepository/UseThirdParty for details.
#
# @param pin
#   Creates a declaration of the apt::pin defined type. Valid options: a number or string to be passed to the `priority` parameter of the
#   `apt::pin` defined type, or a hash of `parameter => value` pairs to be passed to `apt::pin`'s corresponding parameters.
#
# @param architecture
#   Tells Apt to only download information for specified architectures. Valid options: a string containing one or more architecture names,
#   separated by commas (e.g., 'i386' or 'i386,alpha,powerpc').
#   (if unspecified, Apt downloads information for all architectures defined in the Apt::Architectures option)
#   DEB822: Supports an array of values
#
# @param allow_unsigned
#   Specifies whether to authenticate packages from this release, even if the Release file is not signed or the signature can't be checked.
#
# @param allow_insecure
#   Specifies whether to allow downloads from insecure repositories.
#
# @param notify_update
#   Specifies whether to trigger an `apt-get update` run.
#
# @param check_valid_until
#   Specifies whether to check if the package release date is valid.
#
define apt::source (
  Enum['list', 'sources'] $source_format = 'list',
  Array[Enum['deb','deb-src'], 1, 2] $types = ['deb'],
  Optional[Variant[String[1], Array[String[1]]]] $location = undef,
  String[1] $comment = $name,
  Boolean $enabled = true, # deb822
  Enum['present', 'absent'] $ensure = present,
  Optional[Variant[String[0], Array[String[0]]]] $release = undef,
  Variant[String[1], Array[String[1]]] $repos = 'main',
  Hash $include = {},
  Optional[Variant[String[1], Hash]] $key = undef,
  Optional[Stdlib::AbsolutePath] $keyring = undef,
  Optional[Variant[Hash, Integer, String[1]]] $pin = undef,
  Optional[Variant[String[1], Array[String[1]]]] $architecture = undef,
  Optional[Boolean] $allow_unsigned = undef,
  Optional[Boolean] $allow_insecure = undef,
  Optional[Boolean] $check_valid_until = undef,
  Boolean $notify_update = true,
) {
  include apt

  $_before = Apt::Setting["list-${title}"]

  case $source_format {
    'list': {
      $_file_suffix = $source_format

      if !$release {
        if fact('os.distro.codename') {
          $_release = fact('os.distro.codename')
        } else {
          fail('os.distro.codename fact not available: release parameter required')
        }
      } else {
        $_release = $release
      }

      if $release =~ Pattern[/\/$/] {
        $_components = $_release
      } elsif $repos =~ Array {
        $_components = join([$_release] + $repos, ' ')
      } else {
        $_components = "${_release} ${repos}"
      }

      if $ensure == 'present' {
        if ! $location {
          fail('cannot create a source entry without specifying a location')
        }
        elsif ($apt::proxy['https_acng']) and ($location =~ /(?i:^https:\/\/)/) {
          $_location = regsubst($location, 'https://','http://HTTPS///')
        }
        else {
          $_location = $location
        }
      } else {
        $_location = undef
      }

      $includes = $apt::include_defaults + $include

      if $keyring {
        if $key {
          fail('parameters key and keyring are mutually exclusive')
        } else {
          $_list_keyring = $keyring
        }
      } elsif $key {
        if $key =~ Hash {
          unless $key['name'] or $key['id'] {
            fail('key hash must contain a key name (for apt::keyring) or an id (for apt::key)')
          }
          if $key['id'] {
            # defaults like keyserver are only relevant to apt::key
            $_key = $apt::source_key_defaults + $key
          } else {
            $_key = $key
          }
        } else {
          $_key = { 'id' => assert_type(String[1], $key) }
        }
        if $_key['ensure'] {
          $_key_ensure = $_key['ensure']
        } else {
          $_key_ensure = $ensure
        }

        # Old keyserver keys handled by apt-key
        if $_key =~ Hash and $_key['id'] {
          # We do not want to remove keys when the source is absent.
          if $ensure == 'present' {
            apt::key { "Add key: ${$_key['id']} from Apt::Source ${title}":
              ensure   => $_key_ensure,
              id       => $_key['id'],
              server   => $_key['server'],
              content  => $_key['content'],
              source   => $_key['source'],
              options  => $_key['options'],
              weak_ssl => $_key['weak_ssl'],
              before   => $_before,
            }
          }
          $_list_keyring = undef
        }
        # Modern apt keyrings
        elsif $_key =~ Hash and $_key['name'] {
          apt::keyring { $_key['name']:
            ensure   => $_key_ensure,
            content  => $_key['content'],
            source   => $_key['source'],
            dir      => $_key['dir'],
            filename => $_key['filename'],
            mode     => $_key['mode'],
            before   => $_before,
          }

          $_list_keyring = if $_key['dir'] and $_key['filename'] {
            "${_key['dir']}${_key['filename']}"
          } elsif $_key['filename'] {
            "/etc/apt/keyrings/${_key['filename']}"
          } elsif $_key['dir'] {
            "${_key['dir']}${_key['name']}"
          } else {
            "/etc/apt/keyrings/${_key['name']}"
          }
        }
      } else {
        # No `key` nor `keyring` provided
        $_list_keyring = undef
      }

      $header = epp('apt/_header.epp')

      if $architecture {
        $_architecture = regsubst($architecture, '\baarch64\b', 'arm64')
      } else {
        $_architecture = undef
      }

      $source_content = epp('apt/source.list.epp', {
          'comment'          => $comment,
          'includes'         => $includes,
          'options'          => delete_undef_values({
              'arch'              => $_architecture,
              'trusted'           => $allow_unsigned ? { true => 'yes', false => undef, default => undef },
              'allow-insecure'    => $allow_insecure ? { true => 'yes', false => undef, default => undef },
              'signed-by'         => $_list_keyring,
              'check-valid-until' => $check_valid_until? { true => undef, false => 'false', default => undef },
            },
          ),
          'location'         => $_location,
          'components'       => $_components,
        }
      )

      if $pin {
        if $pin =~ Hash {
          $_pin = $pin + { 'ensure' => $ensure, 'before' => $_before }
        } elsif ($pin =~ Numeric or $pin =~ String) {
          $url_split = split($location, '[:\/]+')
          $host      = $url_split[1]
          $_pin = {
            'ensure'   => $ensure,
            'priority' => $pin,
            'before'   => $_before,
            'origin'   => $host,
          }
        } else {
          fail('Received invalid value for pin parameter')
        }

        apt::pin { $name:
          * => $_pin,
        }
      }
    }
    'sources': {
      $_file_suffix = $source_format

      if $pin {
        warning("'pin' parameter is not supported with deb822 format.")
      }
      if $key {
        warning("'key' parameter is not supported with deb822 format.")
      }
      if $ensure == 'present' {
        if ! $location {
          fail('cannot create a source entry without specifying a location')
        }
      }
      if $location !~ Array {
        warning('For deb822 sources, location must be specified as an array.')
        $_location = [$location]
      } else {
        $_location = $location
      }

      if !$release {
        if fact('os.distro.codename') {
          $_release = [fact('os.distro.codename')]
        } else {
          fail('os.distro.codename fact not available: release parameter required')
        }
      } elsif $release !~ Array {
        warning("For deb822 sources, 'release' must be specified as an array. Converting to array.")
        $_release = [$release]
      } else {
        $_release = $release
      }

      if $repos !~ Array {
        warning("For deb822 sources, 'repos' must be specified as an array. Converting to array.")
        $_repos = split($repos, /\s+/)
      } else {
        $_repos = $repos
      }

      if $architecture and $architecture !~ Array {
        warning("For deb822 sources, 'architecture' must be specified as an array. Converting to array.")
        $_architecture = split($architecture, '[,]')
      } else {
        $_architecture = $architecture
      }
      case $ensure {
        'present': {
          $header = epp('apt/_header.epp')
          $source_content = epp('apt/source_deb822.epp', delete_undef_values({
                'uris'              => $_location,
                'suites'            => $_release,
                'components'        => $_repos,
                'types'             => $types,
                'comment'           => $comment,
                'enabled'           => $enabled ? { true => 'yes', false => 'no' },
                'architectures'     => $_architecture,
                'allow_insecure'    => $allow_insecure ? { true => 'yes', false => 'no', default => undef },
                'repo_trusted'      => $allow_unsigned ? { true => 'yes', false => 'no', default => undef },
                'check_valid_until' => $check_valid_until ? { true => 'yes', false => 'no', default => undef },
                'signed_by'         => $keyring,
              }
            )
          )
        }
        'absent': {
          $header = undef
          $source_content = undef
        }
        default: {
          fail('Unexpected value for $ensure parameter.')
        }
      }
    }
    default: {
      fail("Unexpected APT source format: ${source_format}")
    }
  }
  apt::setting { "${_file_suffix}-${name}":
    ensure        => $ensure,
    content       => "${header}${source_content}",
    notify_update => $notify_update,
  }
}
