# -*- coding: utf-8 -*- #
# Copyright 2023 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helpers for parsing flags and arguments."""


import collections
import ipaddress
import re

import frozendict
from googlecloudsdk.api_lib.privateca import base as privateca_base
from googlecloudsdk.api_lib.util import messages as messages_util
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.privateca import exceptions as privateca_exceptions
from googlecloudsdk.command_lib.privateca import preset_profiles
from googlecloudsdk.command_lib.privateca import text_utils
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import times
import six

_NAME_CONSTRAINT_CRITICAL = 'critical'

_NAME_CONSTRAINT_MAPPINGS = frozendict.frozendict({
    'name_permitted_ip': 'permittedIpRanges',
    'name_excluded_ip': 'excludedIpRanges',
    'name_permitted_email': 'permittedEmailAddresses',
    'name_excluded_email': 'excludedEmailAddresses',
    'name_permitted_uri': 'permittedUris',
    'name_excluded_uri': 'excludedUris',
    'name_permitted_dns': 'permittedDnsNames',
    'name_excluded_dns': 'excludedDnsNames',
})

_HIDDEN_KNOWN_EXTENSIONS = frozenset(['name-constraints'])

_USER_DEFINED_ACCESS_URLS_MAPPINGS = frozendict.frozendict({
    'custom_aia_urls': 'aiaIssuingCertificateUrls',
    'custom_cdp_urls': 'crlAccessUrls',
})

_EMAIL_SAN_REGEX = re.compile('^[^@]+@[^@]+$')
# Any number of labels (any character that is not a dot) concatenated by dots
_DNS_SAN_REGEX = re.compile(r'^([^.]+\.)*[^.]+$')
# Subject Key Identifier must be a lowercase hex string.
_SKID_REGEX = re.compile(r'^([0-9a-f][0-9a-f])+$')

# Flag definitions

PUBLISH_CA_CERT_CREATE_HELP = """
If this is enabled, the following will happen:
1) The CA certificates will be written to a known location within the CA distribution point.
2) The AIA extension in all issued certificates will point to the CA cert URL in that distribition point.

Note that the same bucket may be used for the CRLs if --publish-crl is set.
"""

PUBLISH_CA_CERT_UPDATE_HELP = """
If this is enabled, the following will happen:
1) The CA certificates will be written to a known location within the CA distribution point.
2) The AIA extension in all issued certificates will point to the CA cert URL in that distribution point.

If this gets disabled, the AIA extension will not be written to any future certificates issued
by this CA. However, an existing bucket will not be deleted, and the CA certificates will not
be removed from that bucket.

Note that the same bucket may be used for the CRLs if --publish-crl is set.
"""

PUBLISH_CRL_CREATE_HELP = """
If this gets enabled, the following will happen:
1) CRLs will be written to a known location within the CA distribution point.
2) The CDP extension in all future issued certificates will point to the CRL URL in that distribution point.

Note that the same bucket may be used for the CA cert if --publish-ca-cert is set.

CRL publication is not supported for CAs in the DevOps tier.
"""

PUBLISH_CRL_UPDATE_HELP = """
If this gets enabled, the following will happen:
1) CRLs will be written to a known location within the CA distribution point.
2) The CDP extension in all future issued certificates will point to the CRL URL in that distribution point.

If this gets disabled, the CDP extension will not be written to any future certificates issued
by CAs in this pool, and new CRLs will not be published to that bucket (which affects existing certs).
However, an existing bucket will not be deleted, and any existing CRLs will not be removed
from that bucket.

Note that the same bucket may be used for the CA cert if --publish-ca-cert is set.

CRL publication is not supported for CAs in the DevOps tier.
"""

_VALID_KEY_USAGES = [
    'digital_signature',
    'content_commitment',
    'key_encipherment',
    'data_encipherment',
    'key_agreement',
    'cert_sign',
    'crl_sign',
    'encipher_only',
    'decipher_only',
]
_VALID_EXTENDED_KEY_USAGES = [
    'server_auth',
    'client_auth',
    'code_signing',
    'email_protection',
    'time_stamping',
    'ocsp_signing',
]


def AddEncryptionKeyFlag(parser):
  """Adds the encryption key flag.

  Registers the flag for the Cloud KMS key (CMEK) used to encrypt certificates
  in this pool.

  Args:
    parser: The parser to add the flags to.
  """
  base.Argument(
      '--encryption-key',
      help=(
          'The full resource name of the Cloud KMS key to use for encrypting'
          ' certificate data at rest. The key must be in the same region as'
          ' the CA pool.'
      ),
      required=False,
  ).AddToParser(parser)


def AddPublishCrlFlag(parser, use_update_help_text=False):
  help_text = (
      PUBLISH_CRL_UPDATE_HELP
      if use_update_help_text
      else PUBLISH_CRL_CREATE_HELP
  )
  base.Argument(
      '--publish-crl',
      help=help_text,
      action='store_true',
      required=False,
      default=True,
  ).AddToParser(parser)


def AddPublishCaCertFlag(parser, use_update_help_text=False):
  help_text = (
      PUBLISH_CA_CERT_UPDATE_HELP
      if use_update_help_text
      else PUBLISH_CA_CERT_CREATE_HELP
  )
  base.Argument(
      '--publish-ca-cert',
      help=help_text,
      action='store_true',
      required=False,
      default=True,
  ).AddToParser(parser)


def _StripVal(val):
  return six.text_type(val).strip()


def AddUsePresetProfilesFlag(parser):
  base.Argument(
      '--use-preset-profile',
      help=(
          'The name of an existing preset profile used to encapsulate X.509 '
          'parameter values. USE_PRESET_PROFILE must be one of: {}.'
          '\n\nFor more information, see '
          'https://cloud.google.com/certificate-authority-service/docs/certificate-profile.'
      ).format(', '.join(preset_profiles.GetPresetProfileOptions())),
      required=False,
  ).AddToParser(parser)


def AddAutoEnableFlag(parser):
  base.Argument(
      '--auto-enable',
      help=(
          'If this flag is set, the Certificate Authority will be automatically'
          ' enabled upon creation.'
      ),
      action='store_true',
      required=False,
  ).AddToParser(parser)


def _AddSubjectAlternativeNameFlags(parser):
  """Adds the Subject Alternative Name (san) flags.

  This will add --ip-san, --email-san, --dns-san, and --uri-san to the parser.

  Args:
    parser: The parser to add the flags to.
  """
  base.Argument(
      '--email-san',
      help='One or more comma-separated email Subject Alternative Names.',
      type=arg_parsers.ArgList(element_type=_StripVal),
      metavar='EMAIL_SAN',
  ).AddToParser(parser)
  base.Argument(
      '--ip-san',
      help='One or more comma-separated IP Subject Alternative Names.',
      type=arg_parsers.ArgList(element_type=_StripVal),
      metavar='IP_SAN',
  ).AddToParser(parser)
  base.Argument(
      '--dns-san',
      help='One or more comma-separated DNS Subject Alternative Names.',
      type=arg_parsers.ArgList(element_type=_StripVal),
      metavar='DNS_SAN',
  ).AddToParser(parser)
  base.Argument(
      '--uri-san',
      help='One or more comma-separated URI Subject Alternative Names.',
      type=arg_parsers.ArgList(element_type=_StripVal),
      metavar='URI_SAN',
  ).AddToParser(parser)


def _AddSubjectFileFlag(parser):
  base.Argument(
      '--subject-file',
      metavar='SUBJECT_FILE',
      help='A yaml file containing the RDN sequence for the Subject field.',
      hidden=True,
      type=arg_parsers.YAMLFileContents(),
  ).AddToParser(parser)


def _AddSubjectFlag(parser):
  base.Argument(
      '--subject',
      metavar='SUBJECT',
      help=(
          'X.501 name of the certificate subject. Example: --subject '
          '"C=US,ST=California,L=Mountain View,O=Google LLC,CN=google.com"'
      ),
      type=arg_parsers.ArgDict(key_type=_StripVal, value_type=_StripVal),
  ).AddToParser(parser)


def AddSubjectFlags(parser, subject_required=False):
  """Adds subject flags to the parser including subject string and SAN flags.

  Args:
    parser: The parser to add the flags to.
    subject_required: Whether the subject flag should be required.
  """
  subject_group = parser.add_group(
      mutex=True,
      required=subject_required,
  )
  _AddSubjectFlag(subject_group)
  _AddSubjectFileFlag(subject_group)
  _AddSubjectAlternativeNameFlags(parser)


def AddSubjectKeyIdFlag(parser):
  base.Argument(
      '--subject-key-id',
      help=(
          'Optional field to specify subject key ID for certificate. '
          'DO NOT USE except to maintain a previously established identifier '
          'for a public key, whose SKI was not generated using method (1) '
          'described in RFC 5280 section 4.2.1.2.'
      ),
  ).AddToParser(parser)


def AddValidityFlag(
    parser, resource_name, default_value='P10Y', default_value_text='10 years'
):
  base.Argument(
      '--validity',
      help=(
          'The validity of this {}, as an ISO8601 duration. Defaults to {}.'
          .format(resource_name, default_value_text)
      ),
      default=default_value,
  ).AddToParser(parser)


def AddCaPoolIssuancePolicyFlag(parser):
  base.Argument(
      '--issuance-policy',
      action='store',
      type=arg_parsers.YAMLFileContents(),
      help="A YAML file describing this CA Pool's issuance policy.",
  ).AddToParser(parser)


def AddEncodingFormatFlag(parser):
  _ENCODING_FORMAT_MAPPER.choice_arg.AddToParser(parser)


def AddPublishingOptionsFlags(parser, use_update_help_text=False):
  AddPublishCaCertFlag(parser, use_update_help_text)
  AddPublishCrlFlag(parser, use_update_help_text)
  AddEncodingFormatFlag(parser)


def AddBucketFlag(parser):
  base.Argument(
      '--bucket',
      help=(
          'The name of an existing storage bucket to use for storing the CA'
          ' certificates and CRLs for CAs in this pool. If omitted, a new'
          ' bucket will be created and managed by the service on your behalf.'
      ),
      required=False,
  ).AddToParser(parser)


def AddIgnoreActiveCertificatesFlag(parser):
  base.Argument(
      '--ignore-active-certificates',
      help=(
          'If this flag is set, the Certificate Authority will be '
          'deleted even if the Certificate Authority has '
          'un-revoked or un-expired certificates after the grace period.'
      ),
      action='store_true',
      default=False,
      required=False,
  ).AddToParser(parser)


def AddSkipGracePeriodFlag(parser):
  base.Argument(
      '--skip-grace-period',
      help=(
          'If this flag is set, the Certificate Authority will be '
          'deleted as soon as possible without a 30-day grace period where '
          'undeletion would have been allowed. If you proceed, there will be '
          'no way to recover this CA.'
      ),
      action='store_true',
      default=False,
      required=False,
  ).AddToParser(parser)


def AddIgnoreDependentResourcesFlag(parser):
  base.Argument(
      '--ignore-dependent-resources',
      help=(
          'This field skips the integrity check that would normally prevent'
          ' breaking a CA Pool if it is used by another cloud resource and'
          ' allows the CA Pool to be in a state where it is not able to issue'
          ' certificates. Doing so may result in unintended and unrecoverable'
          ' effects on any dependent resource(s) since the CA Pool would not be'
          ' able to issue certificates.'
      ),
      action='store_true',
      default=False,
      required=False,
  ).AddToParser(parser)


def AddNameConstraintParameterFlags(parser):
  """Adds flags for inline name constraint x509 parameters.

  Args:
    parser: The parser to add the flags to.
  """
  base.Argument(
      '--name-constraints-critical',
      help=(
          'Indicates whether or not name constraints are marked as critical. '
          'Name constraints are considered critical unless explicitly set to '
          'false.'
      ),
      default=True,
      action='store_true',
  ).AddToParser(parser)
  base.Argument(
      '--name-permitted-dns',
      help=(
          'One or more comma-separated  DNS names which are permitted to be '
          'issued certificates. Any DNS name that can be constructed by simply '
          'adding zero or more labels to the left-hand side of the name '
          'satisfies the name constraint. For example, `example.com`, '
          '`www.example.com`, `www.sub.example.com` would satisfy '
          '`example.com`, while `example1.com` does not.'
      ),
      metavar='NAME_PERMITTED_DNS',
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)
  base.Argument(
      '--name-excluded-dns',
      metavar='NAME_EXCLUDED_DNS',
      help=(
          'One or more comma-separated DNS names which are excluded from '
          'being issued certificates. Any DNS name that can be constructed by '
          'simply adding zero or more labels to the left-hand side of the name '
          'satisfies the name constraint. For example, `example.com`, '
          '`www.example.com`, `www.sub.example.com` would satisfy '
          '`example.com`, while `example1.com` does not.'
      ),
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)
  base.Argument(
      '--name-permitted-ip',
      metavar='NAME_PERMITTED_IP',
      help=(
          'One or more comma-separated IP ranges which are permitted to be '
          'issued certificates. For IPv4 addresses, the ranges are expressed '
          'using CIDR notation as specified in RFC 4632. For IPv6 addresses, '
          'the ranges are expressed in similar encoding as IPv4'
      ),
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)
  base.Argument(
      '--name-excluded-ip',
      metavar='NAME_EXCLUDED_IP',
      help=(
          'One or more comma-separated IP ranges which are excluded from being '
          'issued certificates. For IPv4 addresses, the ranges are expressed '
          'using CIDR notation as specified in RFC 4632. For IPv6 addresses, '
          'the ranges are expressed in similar encoding as IPv4'
      ),
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)
  base.Argument(
      '--name-permitted-email',
      metavar='NAME_PERMITTED_EMAIL',
      help=(
          'One or more comma-separated email addresses which are permitted to '
          'be issued certificates. The value can be a particular email '
          'address, a hostname to indicate all email addresses on that host or '
          'a domain with a leading period (e.g. `.example.com`) to indicate '
          'all email addresses in that domain.'
      ),
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)
  base.Argument(
      '--name-excluded-email',
      metavar='NAME_EXCLUDED_EMAIL',
      help=(
          'One or more comma-separated emails which are excluded from being '
          'issued certificates. The value can be a particular email '
          'address, a hostname to indicate all email addresses on that host or '
          'a domain with a leading period (e.g. `.example.com`) to indicate '
          'all email addresses in that domain.'
      ),
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)
  base.Argument(
      '--name-permitted-uri',
      help=(
          'One or more comma-separated URIs which are permitted to be issued '
          'certificates. The value can be a hostname or a domain with a '
          'leading period (like `.example.com`)'
      ),
      metavar='NAME_PERMITTED_URI',
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)
  base.Argument(
      '--name-excluded-uri',
      metavar='NAME_EXCLUDED_URI',
      help=(
          'One or more comma-separated URIs which are excluded from being '
          'issued certificates. The value can be a hostname or a domain with '
          'a leading period (like `.example.com`)'
      ),
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)


def AddInlineX509ParametersFlags(
    parser, is_ca_command, default_max_chain_length=None
):
  """Adds flags for providing inline x509 parameters.

  Args:
    parser: The parser to add the flags to.
    is_ca_command: Whether the current command is on a CA. This influences the
      help text, and whether the --is-ca-cert flag is added.
    default_max_chain_length: optional, The default value for maxPathLength to
      use if an explicit value is not specified. If this is omitted or set to
      None, no default max path length will be added.
  """
  resource_name = 'CA' if is_ca_command else 'certificate'
  group = parser.add_group()
  base.Argument(
      '--key-usages',
      metavar='KEY_USAGES',
      help=(
          'The list of key usages for this {}. This can only be provided if'
          ' `--use-preset-profile` is not provided.'.format(resource_name)
      ),
      type=arg_parsers.ArgList(
          element_type=_StripVal, choices=_VALID_KEY_USAGES
      ),
  ).AddToParser(group)
  base.Argument(
      '--extended-key-usages',
      metavar='EXTENDED_KEY_USAGES',
      help=(
          'The list of extended key usages for this {}. This can only be'
          ' provided if `--use-preset-profile` is not provided.'.format(
              resource_name
          )
      ),
      type=arg_parsers.ArgList(
          element_type=_StripVal, choices=_VALID_EXTENDED_KEY_USAGES
      ),
  ).AddToParser(group)

  name_constraints_group = group.add_group(
      help='The x509 name constraints configurations'
  )
  AddNameConstraintParameterFlags(name_constraints_group)

  chain_length_group = group.add_group(mutex=True)
  base.Argument(
      '--max-chain-length',
      help=(
          'Maximum depth of subordinate CAs allowed under this CA for a CA'
          ' certificate. This can only be provided if neither'
          ' `--use-preset-profile` nor `--unconstrained-chain-length` are'
          ' provided.'
      ),
      default=default_max_chain_length,
  ).AddToParser(chain_length_group)
  base.Argument(
      '--unconstrained-chain-length',
      help=(
          'If set, allows an unbounded number of subordinate CAs under this'
          ' newly issued CA certificate. This can only be provided if neither'
          ' `--use-preset-profile` nor `--max-chain-length` are provided.'
      ),
      action='store_true',
  ).AddToParser(chain_length_group)

  if not is_ca_command:
    base.Argument(
        '--is-ca-cert',
        help=(
            'Whether this certificate is for a CertificateAuthority or not.'
            ' Indicates the Certificate Authority field in the x509 basic'
            ' constraints extension.'
        ),
        required=False,
        default=False,
        action='store_true',
    ).AddToParser(group)


def AddIdentityConstraintsFlags(parser, require_passthrough_flags=True):
  """Adds flags for expressing identity constraints.

  Args:
    parser: The argparse object to add the flags to.
    require_passthrough_flags: Whether the boolean --copy-* flags should be
      required.
  """

  base.Argument(
      '--identity-cel-expression',
      help=(
          'A CEL expression that will be evaluated against the identity '
          'in the certificate before it is issued, and returns a boolean '
          'signifying whether the request should be allowed.'
      ),
  ).AddToParser(parser)

  base.Argument(
      '--copy-subject',
      help=(
          'If this is specified, the Subject from the certificate request '
          'will be copied into the signed certificate. Specify '
          '--no-copy-subject to drop any caller-specified subjects from the '
          'certificate request.'
      ),
      action='store_true',
      required=require_passthrough_flags,
  ).AddToParser(parser)
  base.Argument(
      '--copy-sans',
      help=(
          'If this is specified, the Subject Alternative Name extension from '
          'the certificate request will be copied into the signed certificate. '
          'Specify --no-copy-sans to drop any caller-specified SANs in the '
          'certificate request.'
      ),
      action='store_true',
      required=require_passthrough_flags,
  ).AddToParser(parser)


def AddUserDefinedAccessUrlsFlags(parser):
  """Adds flags for specifying user defined access URLs, such as CDP and AIA.

  Args:
    parser: The parser to add the flags to.
  """
  base.Argument(
      '--custom-aia-urls',
      help=(
          'One or more comma-separated URLs that will be added to the Authority'
          ' Information Access extension in the issued certificate.'
          ' These URLs are where the issuer CA certificate is located.'
      ),
      metavar='CUSTOM_AIA_URLS',
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)
  base.Argument(
      '--custom-cdp-urls',
      help=(
          'One or more comma-separated URLs that will be added to the CRL'
          ' Distribution Points (CDP) extension in the issued certificate.'
          ' These URLs are where CRL information is located.'
      ),
      metavar='CUSTOM_CDP_URLS',
      type=arg_parsers.ArgList(element_type=_StripVal),
  ).AddToParser(parser)


def GetKnownExtensionMapping():
  enum_type = privateca_base.GetMessagesModule(
      'v1'
  ).CertificateExtensionConstraints.KnownExtensionsValueListEntryValuesEnum
  return collections.OrderedDict((
      ('base-key-usage', enum_type.BASE_KEY_USAGE),
      ('extended-key-usage', enum_type.EXTENDED_KEY_USAGE),
      ('ca-options', enum_type.CA_OPTIONS),
      ('policy-ids', enum_type.POLICY_IDS),
      ('aia-ocsp-servers', enum_type.AIA_OCSP_SERVERS),
      ('name-constraints', enum_type.NAME_CONSTRAINTS),
  ))


def _StrToObjectId(val):
  return privateca_base.GetMessagesModule('v1').ObjectId(
      objectIdPath=[int(part) for part in six.text_type(val).strip().split('.')]
  )


def _StrToKnownExtension(arg_name, val):
  trimmed_val = six.text_type(val).strip().lower()
  known_extensions = GetKnownExtensionMapping()
  if trimmed_val in known_extensions:
    return known_extensions[trimmed_val]
  else:
    raise exceptions.UnknownArgumentException(
        arg_name,
        'expected one of [{}]'.format(','.join(known_extensions.keys())),
    )


def AddExtensionConstraintsFlags(parser):
  """Adds flags for expressing extension constraints.

  Args:
    parser: The argparser to add the arguments to.
  """
  extension_group = parser.add_group(
      mutex=True,
      required=False,
      help=(
          'Constraints on requested X.509 extensions. If unspecified, all '
          'extensions from certificate request will be ignored when signing '
          'the certificate.'
      ),
  )

  copy_group = extension_group.add_group(
      mutex=False,
      required=False,
      help='Specify exact x509 extensions to copy by OID or known extension.',
  )

  base.Argument(
      '--copy-extensions-by-oid',
      help=(
          'If this is set, then extensions with the given OIDs will be copied '
          'from the certificate request into the signed certificate.'
      ),
      type=arg_parsers.ArgList(element_type=_StrToObjectId),
      metavar='OBJECT_ID',
  ).AddToParser(copy_group)
  known_extensions = GetKnownExtensionMapping()
  base.Argument(
      '--copy-known-extensions',
      help=(
          'If this is set, then the given extensions will be copied '
          'from the certificate request into the signed certificate.'
      ),
      type=arg_parsers.ArgList(
          choices=known_extensions.keys(),
          hidden_choices=_HIDDEN_KNOWN_EXTENSIONS,
      ),
      metavar='KNOWN_EXTENSIONS',
  ).AddToParser(copy_group)

  base.Argument(
      '--copy-all-requested-extensions',
      help=(
          'If this is set, all extensions specified in the certificate '
          ' request will be copied into the signed certificate.'
      ),
      action='store_const',
      const=True,
  ).AddToParser(extension_group)


def AddMaximumLifetimeFlag(parser):
  """Adds flag for specifying maximum lifetime in cert template.

  Args:
    parser: The argparser to add the arguments to.
  """

  base.Argument(
      '--maximum-lifetime',
      help=(
          "If this is set, then issued certificate's lifetime will be truncated"
          " to the value provided. If the issuing CaPool's IssuancePolicy"
          ' specifies a maximum lifetime the minimum of the two durations will'
          ' be the maximum lifetime for the issued certificate. Note that if'
          " the issuing CertificateAuthority expires before a Certificate's"
          ' requested maximum_lifetime, the effective lifetime will be'
          ' explicitly truncated to match it.'
      ),
  ).AddToParser(parser)


def AddExtensionConstraintsFlagsForUpdate(parser):
  """Adds flags for updating extension constraints.

  Args:
    parser: The argparser to add the arguments to.
  """
  extension_group_help = 'Constraints on requested X.509 extensions.'

  extension_group = parser.add_group(
      mutex=True, required=False, help=extension_group_help
  )
  copy_group = extension_group.add_group(
      mutex=False,
      required=False,
      help='Specify exact x509 extensions to copy by OID or known extension.',
  )

  oid_group = copy_group.add_group(
      mutex=True,
      required=False,
      help='Constraints on unknown extensions by their OIDs.',
  )

  base.Argument(
      '--copy-extensions-by-oid',
      help=(
          'If this is set, then extensions with the given OIDs will be copied '
          'from the certificate request into the signed certificate.'
      ),
      type=arg_parsers.ArgList(element_type=_StrToObjectId),
      metavar='OBJECT_ID',
  ).AddToParser(oid_group)
  base.Argument(
      '--drop-oid-extensions',
      help=(
          'If this is set, then all existing OID extensions will be removed '
          'from the template, prohibiting any extensions specified by '
          'OIDs to be specified by the requester.'
      ),
      action='store_const',
      const=True,
  ).AddToParser(oid_group)

  known_group = copy_group.add_group(
      mutex=True, required=False, help='Constraints on known extensions.'
  )

  known_extensions = GetKnownExtensionMapping()
  base.Argument(
      '--copy-known-extensions',
      help=(
          'If this is set, then the given extensions will be copied '
          'from the certificate request into the signed certificate.'
      ),
      type=arg_parsers.ArgList(
          choices=known_extensions.keys(),
          hidden_choices=_HIDDEN_KNOWN_EXTENSIONS,
      ),
      metavar='KNOWN_EXTENSIONS',
  ).AddToParser(known_group)
  base.Argument(
      '--drop-known-extensions',
      help=(
          'If this is set, then all known extensions will be '
          'removed from the template, prohibiting any known x509 extensions to '
          'be specified by the requester.'
      ),
      action='store_const',
      const=True,
  ).AddToParser(known_group)

  base.Argument(
      '--copy-all-requested-extensions',
      help=(
          'If this is set, all extensions, whether known or specified by '
          'OID, that are specified in the certificate request will be copied '
          'into the signed certificate.'
      ),
      action='store_const',
      const=True,
  ).AddToParser(extension_group)


def AddPredefinedValuesFileFlag(parser):
  """Adds a flag for the predefined x509 extensions file for a Certificate Template."""
  # This string contains URLs which shouldn't be split across lines.
  # pylint: disable=line-too-long
  base.Argument(
      '--predefined-values-file',
      action='store',
      type=arg_parsers.YAMLFileContents(),
      help=(
          'A YAML file describing any predefined X.509 values set by this '
          'template. The provided extensions will be copied over to any '
          'certificate requests that use this template, taking precedent '
          'over any allowed extensions in the certificate request. The '
          'format of this file should be a YAML representation of the '
          'X509Parameters message, which is defined here: '
          'https://cloud.google.com/certificate-authority-service/docs/reference/rest/v1/X509Parameters'
          '. Some examples can be found here: '
          'https://cloud.google.com/certificate-authority-service/docs/creating-certificate-template'
      ),
  ).AddToParser(parser)


# Flag parsing


def ParseEncryptionSpec(args):
  """Parses the encryption-key flag from args."""
  if not args.IsSpecified('encryption_key'):
    return None

  messages = privateca_base.GetMessagesModule('v1')

  return messages.EncryptionSpec(cloudKmsKey=args.encryption_key)


def ParseIdentityConstraints(args):
  """Parses the identity flags into a CertificateIdentityConstraints object."""
  messages = privateca_base.GetMessagesModule('v1')

  return messages.CertificateIdentityConstraints(
      allowSubjectPassthrough=args.copy_subject,
      allowSubjectAltNamesPassthrough=args.copy_sans,
      celExpression=messages.Expr(expression=args.identity_cel_expression)
      if args.IsSpecified('identity_cel_expression')
      else None,
  )


def ParseExtensionConstraints(args):
  """Parse extension constraints flags into CertificateExtensionConstraints API message.

  Assumes that the parser defined by args has the flags
  copy_all_requested_extensions, copy_known_extesnions, and
  copy-extensions-by-oid. Also supports drop_known_extensions and
  drop_oid_extensions for clearing the extension lists.

  Args:
    args: The argparse object to read flags from.

  Returns:
    The CertificateExtensionConstraints API message.
  """
  if args.IsSpecified('copy_all_requested_extensions'):
    return None

  messages = privateca_base.GetMessagesModule('v1')
  known_exts = []
  if not args.IsKnownAndSpecified('drop_known_extensions') and args.IsSpecified(
      'copy_known_extensions'
  ):
    known_exts = [
        _StrToKnownExtension('--copy-known-extensions', ext)
        for ext in args.copy_known_extensions
    ]

  oids = []
  if not args.IsKnownAndSpecified('drop_oid_extensions') and args.IsSpecified(
      'copy_extensions_by_oid'
  ):
    oids = args.copy_extensions_by_oid

  return messages.CertificateExtensionConstraints(
      knownExtensions=known_exts, additionalExtensions=oids
  )


def ParseMaximumLifetime(args):
  """Parses the maximum_lifetime flag from args.

  Args:
    args: The argparse object to read flags from.

  Returns:
    The JSON formatted duration of the maximum lifetime or none.
  """

  if not args.IsSpecified('maximum_lifetime'):
    return None
  return times.FormatDurationForJson(times.ParseDuration(args.maximum_lifetime))


def ParsePredefinedValues(args):
  """Parses an X509Parameters proto message from the predefined values file in args."""
  if not args.IsSpecified('predefined_values_file'):
    return None
  try:
    return messages_util.DictToMessageWithErrorCheck(
        args.predefined_values_file,
        privateca_base.GetMessagesModule('v1').X509Parameters,
    )
  # TODO(b/77547931): Catch `AttributeError` until upstream library takes the
  # fix.
  except (messages_util.DecodeError, AttributeError):
    raise exceptions.InvalidArgumentException(
        '--predefined-values-file',
        'Unrecognized field in the X509Parameters file.',
    )


def ParseSubject(args):
  """Parses a dictionary with subject attributes into a API Subject type.

  Args:
    args: The argparse namespace that contains the flag values.

  Returns:
    Subject: the Subject type represented in the api.
  """
  subject_args = args.subject
  remap_args = {
      'CN': 'commonName',
      'C': 'countryCode',
      'ST': 'province',
      'L': 'locality',
      'O': 'organization',
      'OU': 'organizationalUnit',
  }

  mapped_args = {}
  for key, val in subject_args.items():
    if key in remap_args:
      mapped_args[remap_args[key]] = val
    else:
      mapped_args[key] = val

  try:
    return messages_util.DictToMessageWithErrorCheck(
        mapped_args, privateca_base.GetMessagesModule('v1').Subject
    )
  except messages_util.DecodeError:
    raise exceptions.InvalidArgumentException(
        '--subject', 'Unrecognized subject attribute.'
    )


def ParseSanFlags(args):
  """Validates the san flags and creates a SubjectAltNames message from them.

  Args:
    args: The parser that contains the flags.

  Returns:
    The SubjectAltNames message with the flag data.
  """
  email_addresses, dns_names, ip_addresses, uris = [], [], [], []

  if args.IsSpecified('email_san'):
    email_addresses = list(map(ValidateEmailSanFlag, args.email_san))
  if args.IsSpecified('dns_san'):
    dns_names = list(map(ValidateDnsSanFlag, args.dns_san))
  if args.IsSpecified('ip_san'):
    ip_addresses = list(map(ValidateIpSanFlag, args.ip_san))
  if args.IsSpecified('uri_san'):
    uris = args.uri_san

  return privateca_base.GetMessagesModule('v1').SubjectAltNames(
      emailAddresses=email_addresses,
      dnsNames=dns_names,
      ipAddresses=ip_addresses,
      uris=uris,
  )


def ValidateIdentityConstraints(
    args, existing_copy_subj=False, existing_copy_sans=False, for_update=False
):
  """Validates the template identity constraints flags.

  Args:
    args: the parser for the flag. Expected to have copy_sans and copy_subject
      registered as flags
    existing_copy_subj: A pre-existing value for the subject value, if
      applicable.
    existing_copy_sans: A pre-existing value for the san value, if applicable.
    for_update: Whether the validation is for an update to a template.
  """
  copy_san = args.copy_sans or (
      not args.IsSpecified('copy_sans') and existing_copy_sans
  )
  copy_subj = args.copy_subject or (
      not args.IsSpecified('copy_subject') and existing_copy_subj
  )

  if for_update:
    missing_identity_conf_msg = (
        'The resulting updated template will have no subject or SAN '
        'passthroughs. '
    )
  else:
    missing_identity_conf_msg = (
        'Neither copy-sans nor copy-subject was specified. '
    )
  missing_identity_conf_msg += (
      'This means that all certificate requests that use this template must '
      'use identity reflection.'
  )
  if (
      not copy_san
      and not copy_subj
      and not console_io.PromptContinue(
          message=missing_identity_conf_msg, default=True
      )
  ):
    raise privateca_exceptions.UserAbortException('Aborted by user.')


def ValidateSubjectConfig(subject_config):
  """Validates a SubjectConfig object."""
  san_names = []
  if subject_config.subjectAltName:
    san_names = [
        subject_config.subjectAltName.emailAddresses,
        subject_config.subjectAltName.dnsNames,
        subject_config.subjectAltName.ipAddresses,
        subject_config.subjectAltName.uris,
    ]
  if (
      not subject_config.subject.commonName
      and all([not elem for elem in san_names])
      and not subject_config.subject.rdnSequence
  ):
    raise exceptions.InvalidArgumentException(
        '--subject',
        'The certificate you are creating does not contain a common name or a'
        ' subject alternative name.',
    )


def ParseSubjectFlags(args):
  """Parses subject flags into a subject config.

  Args:
    args: The parser that contains all the flag values

  Returns:
    A subject config representing the parsed flags.
  """
  messages = privateca_base.GetMessagesModule('v1')
  subject_config = messages.SubjectConfig(
      subject=messages.Subject(), subjectAltName=messages.SubjectAltNames()
  )

  if args.IsSpecified('subject'):
    subject_config.subject = ParseSubject(args)
  elif args.IsSpecified('subject_file'):
    subject_config.subject = ParseSubjectFile(args)
  if SanFlagsAreSpecified(args):
    subject_config.subjectAltName = ParseSanFlags(args)

  ValidateSubjectConfig(subject_config)

  return subject_config


def SanFlagsAreSpecified(args):
  """Returns true if any san flags are specified."""
  return any([
      flag in vars(args) and args.IsSpecified(flag)
      for flag in ['dns_san', 'email_san', 'ip_san', 'uri_san']
  ])


def ParseIssuancePolicy(args):
  """Parses an IssuancePolicy proto message from the args."""
  if not args.IsSpecified('issuance_policy'):
    return None
  try:
    return messages_util.DictToMessageWithErrorCheck(
        args.issuance_policy,
        privateca_base.GetMessagesModule('v1').IssuancePolicy,
    )
  # TODO(b/77547931): Catch `AttributeError` until upstream library takes the
  # fix.
  except (messages_util.DecodeError, AttributeError):
    raise exceptions.InvalidArgumentException(
        '--issuance-policy', 'Unrecognized field in the Issuance Policy.'
    )


def ParseSubjectFile(args):
  """Parses an a Subject from a file to a proto message from the args."""
  try:
    return messages_util.DictToMessageWithErrorCheck(
        args.subject_file,
        privateca_base.GetMessagesModule('v1').Subject,
    )

  # TODO(b/77547931): Catch `AttributeError` until upstream library takes the
  # fix.
  except (messages_util.DecodeError, AttributeError):
    raise exceptions.InvalidArgumentException(
        '--subject-file', 'Unrecognized field in the Subject.'
    )


def ParseEncodingFormatFlag(args):
  return _ENCODING_FORMAT_MAPPER.GetEnumForChoice(
      args.publishing_encoding_format
  )


def ParsePublishingOptions(args):
  """Parses the PublshingOptions proto message from the args."""
  messages = privateca_base.GetMessagesModule('v1')
  publish_ca_cert = args.publish_ca_cert
  publish_crl = args.publish_crl
  encoding_format = ParseEncodingFormatFlag(args)

  is_devops_tier = args.IsKnownAndSpecified('tier') and (
      ParseTierFlag(args) == messages.CaPool.TierValueValuesEnum.DEVOPS
  )
  if is_devops_tier:
    if args.IsSpecified('publish_crl') and publish_crl:
      raise exceptions.InvalidArgumentException(
          '--publish-crl',
          'CRL publication is not supported in the DevOps tier.',
      )
    # It's not explicitly set to True, so change the default to False here.
    publish_crl = False

  return messages.PublishingOptions(
      publishCaCert=publish_ca_cert,
      publishCrl=publish_crl,
      encodingFormat=encoding_format,
  )


# Flag validation helpers


def ValidateEmailSanFlag(san):
  if not re.match(_EMAIL_SAN_REGEX, san):
    raise exceptions.InvalidArgumentException(
        '--email-san', 'Invalid email address.'
    )
  return san


def ValidateDnsSanFlag(san):
  if not re.match(_DNS_SAN_REGEX, san):
    raise exceptions.InvalidArgumentException(
        '--dns-san', 'Invalid domain name value'
    )
  return san


def ValidateIpSanFlag(san):
  try:
    ipaddress.ip_address(san)
  except ValueError:
    raise exceptions.InvalidArgumentException(
        '--ip-san', 'Invalid IP address value.'
    )
  return san


_REVOCATION_MAPPING = {
    'REVOCATION_REASON_UNSPECIFIED': 'unspecified',
    'KEY_COMPROMISE': 'key-compromise',
    'CERTIFICATE_AUTHORITY_COMPROMISE': 'certificate-authority-compromise',
    'AFFILIATION_CHANGED': 'affiliation-changed',
    'SUPERSEDED': 'superseded',
    'CESSATION_OF_OPERATION': 'cessation-of-operation',
    'CERTIFICATE_HOLD': 'certificate-hold',
    'PRIVILEGE_WITHDRAWN': 'privilege-withdrawn',
    'ATTRIBUTE_AUTHORITY_COMPROMISE': 'attribute-authority-compromise',
}

_REVOCATION_REASON_MAPPER = arg_utils.ChoiceEnumMapper(
    arg_name='--reason',
    default='unspecified',
    help_str='Revocation reason to include in the CRL.',
    message_enum=privateca_base.GetMessagesModule(
        'v1'
    ).RevokeCertificateRequest.ReasonValueValuesEnum,
    custom_mappings=_REVOCATION_MAPPING,
)

_ENCODING_FORMAT_MAPPING = {
    'PEM': 'pem',
    'DER': 'der',
}

_ENCODING_FORMAT_MAPPER = arg_utils.ChoiceEnumMapper(
    arg_name='--publishing-encoding-format',
    default='pem',
    help_str='The encoding format of the content published to storage buckets.',
    message_enum=privateca_base.GetMessagesModule(
        'v1'
    ).PublishingOptions.EncodingFormatValueValuesEnum,
    custom_mappings=_ENCODING_FORMAT_MAPPING,
)

_TIER_MAPPING = {
    'ENTERPRISE': 'enterprise',
    'DEVOPS': 'devops',
}

_TIER_MAPPER = arg_utils.ChoiceEnumMapper(
    arg_name='--tier',
    default='enterprise',
    help_str='The tier for the Certificate Authority.',
    message_enum=privateca_base.GetMessagesModule(
        'v1'
    ).CaPool.TierValueValuesEnum,
    custom_mappings=_TIER_MAPPING,
)

_KEY_ALGORITHM_MAPPING = {
    'RSA_PSS_2048_SHA256': 'rsa-pss-2048-sha256',
    'RSA_PSS_3072_SHA256': 'rsa-pss-3072-sha256',
    'RSA_PSS_4096_SHA256': 'rsa-pss-4096-sha256',
    'RSA_PKCS1_2048_SHA256': 'rsa-pkcs1-2048-sha256',
    'RSA_PKCS1_3072_SHA256': 'rsa-pkcs1-3072-sha256',
    'RSA_PKCS1_4096_SHA256': 'rsa-pkcs1-4096-sha256',
    'EC_P256_SHA256': 'ec-p256-sha256',
    'EC_P384_SHA384': 'ec-p384-sha384',
}

_KEY_ALGORITHM_MAPPER = arg_utils.ChoiceEnumMapper(
    arg_name='--key-algorithm',
    help_str=(
        'The crypto algorithm to use for creating a managed KMS key for '
        'the Certificate Authority.'
    ),
    message_enum=privateca_base.GetMessagesModule(
        'v1'
    ).KeyVersionSpec.AlgorithmValueValuesEnum,
    custom_mappings=_KEY_ALGORITHM_MAPPING,
)


def AddRevocationReasonFlag(parser):
  """Add a revocation reason enum flag to the parser.

  Args:
    parser: The argparse parser to add the flag to.
  """
  _REVOCATION_REASON_MAPPER.choice_arg.AddToParser(parser)


def ParseRevocationChoiceToEnum(choice):
  """Return the apitools revocation reason enum value from the string choice.

  Args:
    choice: The string value of the revocation reason.

  Returns:
    The revocation enum value for the choice text.
  """
  return _REVOCATION_REASON_MAPPER.GetEnumForChoice(choice)


def ParseValidityFlag(args):
  """Parses the validity from args."""
  return times.FormatDurationForJson(times.ParseDuration(args.validity))


def AddTierFlag(parser):
  _TIER_MAPPER.choice_arg.AddToParser(parser)


def ParseTierFlag(args):
  return _TIER_MAPPER.GetEnumForChoice(args.tier)


def AddKeyAlgorithmFlag(parser_group, default='rsa-pkcs1-4096-sha256'):
  _KEY_ALGORITHM_MAPPER.choice_arg.AddToParser(parser_group)
  _KEY_ALGORITHM_MAPPER.choice_arg.SetDefault(parser_group, default)


def ParseKeySpec(args):
  """Parses a specified KMS key version or algorithm to get a KeyVersionSpec."""
  messages = privateca_base.GetMessagesModule('v1')
  if args.IsSpecified('kms_key_version'):
    kms_key_version_ref = args.CONCEPTS.kms_key_version.Parse()
    return messages.KeyVersionSpec(
        cloudKmsKeyVersion=kms_key_version_ref.RelativeName()
    )

  return messages.KeyVersionSpec(
      algorithm=_KEY_ALGORITHM_MAPPER.GetEnumForChoice(args.key_algorithm)
  )


def ParseNameConstraints(args, messages):
  """Parses the name constraints in x509Parameters.

  Args:
    args: The parsed argument values
    messages: PrivateCA's messages modules

  Returns:
    A NameConstraints message object
  """
  name_constraint_dict = {}
  for constraint_arg, constraint in _NAME_CONSTRAINT_MAPPINGS.items():
    if args.IsKnownAndSpecified(constraint_arg):
      name_constraint_dict[constraint] = getattr(args, constraint_arg)
  if not name_constraint_dict:
    return None
  # Always set critical for name constraints even if the arg is not specified
  # in which case we use the default value.
  name_constraint_dict[_NAME_CONSTRAINT_CRITICAL] = (
      args.name_constraints_critical
  )
  return messages_util.DictToMessageWithErrorCheck(
      name_constraint_dict, message_type=messages.NameConstraints
  )


def ParseSubjectKeyId(args, messages):
  """Parses the subject key id for input into CertificateConfig.

  Args:
    args: The parsed argument values
    messages: PrivateCA's messages modules

  Returns:
    A CertificateConfigKeyId message object
  """
  if not args.IsSpecified('subject_key_id'):
    return None

  skid = args.subject_key_id
  if not re.match(_SKID_REGEX, skid):
    raise exceptions.InvalidArgumentException(
        '--subject-key-id',
        'Subject key id must be an even length lowercase hex string.',
    )
  return messages.CertificateConfigKeyId(keyId=skid)


def ParseX509Parameters(args, is_ca_command):
  """Parses the X509 parameters flags into an API X509Parameters.

  Args:
    args: The parsed argument values.
    is_ca_command: Whether the current command is on a CA. If so, certSign and
      crlSign key usages are added.

  Returns:
    An X509Parameters object.
  """
  preset_profile_set = args.IsKnownAndSpecified('use_preset_profile')

  inline_args = [
      'key_usages',
      'extended_key_usages',
      'max_chain_length',
      'is_ca_cert',
      'unconstrained_chain_length',
  ] + list(_NAME_CONSTRAINT_MAPPINGS.keys())

  # TODO(b/183243757): Change to args.IsSpecified once --use-preset-profile flag
  # is registered.
  has_inline_values = any(
      [args.IsKnownAndSpecified(flag) for flag in inline_args]
  )

  if preset_profile_set and has_inline_values:
    raise exceptions.InvalidArgumentException(
        '--use-preset-profile',
        '--use-preset-profile may not be specified if one or more of '
        '--key-usages, --extended-key-usages, --unconstrained_chain_length or '
        '--max-chain-length are specified.',
    )
  if preset_profile_set:
    return preset_profiles.GetPresetX509Parameters(args.use_preset_profile)

  if args.unconstrained_chain_length:
    args.max_chain_length = None
  base_key_usages = args.key_usages or []
  is_ca = is_ca_command or (
      args.IsKnownAndSpecified('is_ca_cert') and args.is_ca_cert
  )
  if is_ca:
    # A CA should have these KeyUsages to be RFC 5280 compliant.
    base_key_usages.extend(['cert_sign', 'crl_sign'])
  key_usage_dict = {}
  for key_usage in base_key_usages:
    key_usage = text_utils.SnakeCaseToCamelCase(key_usage)
    key_usage_dict[key_usage] = True
  extended_key_usage_dict = {}
  for extended_key_usage in args.extended_key_usages or []:
    extended_key_usage = text_utils.SnakeCaseToCamelCase(extended_key_usage)
    extended_key_usage_dict[extended_key_usage] = True

  messages = privateca_base.GetMessagesModule('v1')
  return messages.X509Parameters(
      keyUsage=messages.KeyUsage(
          baseKeyUsage=messages_util.DictToMessageWithErrorCheck(
              key_usage_dict, messages.KeyUsageOptions
          ),
          extendedKeyUsage=messages_util.DictToMessageWithErrorCheck(
              extended_key_usage_dict, messages.ExtendedKeyUsageOptions
          ),
      ),
      caOptions=messages.CaOptions(
          isCa=is_ca,
          # Don't include maxIssuerPathLength if it's None.
          maxIssuerPathLength=int(args.max_chain_length)
          if is_ca and args.max_chain_length is not None
          else None,
      ),
      nameConstraints=ParseNameConstraints(args, messages),
  )


def X509ConfigFlagsAreSpecified(args):
  """Returns true if any x509 config flags are specified."""
  return any([
      flag in vars(args) and args.IsSpecified(flag)
      for flag in [
          'use_preset_profile',
          'key_usages',
          'extended_key_usages',
          'max_chain_length',
          'unconstrained_chain_length',
          'is_ca_cert',
      ]
  ])


def ParseUserDefinedAccessUrls(args, messages):
  """Parses the user defined access URLs into a UserDefinedAccessUrls message.

  Args:
    args: The parsed argument values
    messages: PrivateCA's messages modules

  Returns:
    A UserDefinedAccessUrls message object
  """
  user_defined_access_urls = {}
  for url_arg, url_field in _USER_DEFINED_ACCESS_URLS_MAPPINGS.items():
    if args.IsKnownAndSpecified(url_arg):
      user_defined_access_urls[url_field] = getattr(args, url_arg)
  if not user_defined_access_urls:
    return None
  return messages_util.DictToMessageWithErrorCheck(
      user_defined_access_urls, message_type=messages.UserDefinedAccessUrls
  )
