# -*- coding: utf-8 -*- #
# Copyright 2022 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.
"""Validate that a terraform plan complies with policies."""

import os.path

from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.util.anthos import binary_operations
from googlecloudsdk.core import log
from googlecloudsdk.core import metrics
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import progress_tracker
from googlecloudsdk.core.credentials.store import GetFreshAccessToken
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import files

MISSING_BINARY = ('Could not locate terraform-tools executable [{binary}]. '
                  'Please ensure gcloud terraform-tools component is '
                  'properly installed. '
                  'See https://cloud.google.com/sdk/docs/components for '
                  'more details.')


class TerraformToolsTfplanToCaiOperation(
    binary_operations.StreamingBinaryBackedOperation):
  """Streaming operation for Terraform Tools tfplan-to-cai command."""
  custom_errors = {}

  def __init__(self, **kwargs):
    custom_errors = {
        'MISSING_EXEC': MISSING_BINARY.format(binary='terraform-tools'),
    }
    super(TerraformToolsTfplanToCaiOperation, self).__init__(
        binary='terraform-tools',
        check_hidden=True,
        install_if_missing=True,
        custom_errors=custom_errors,
        structured_output=True,
        **kwargs)

  def _ParseArgsForCommand(self, command, terraform_plan_json, project, region,
                           zone, verbosity, output_path, **kwargs):
    args = [
        command,
        terraform_plan_json,
        '--output-path',
        output_path,
        '--verbosity',
        verbosity,
        '--user-agent',
        metrics.GetUserAgent(),
    ]
    if project:
      args += ['--project', project]
    if region:
      args += ['--region', region]
    if zone:
      args += ['--zone', zone]
    return args


class TerraformToolsValidateOperation(binary_operations.BinaryBackedOperation):
  """operation for Terraform Tools validate-cai command."""
  custom_errors = {}

  def __init__(self, **kwargs):
    custom_errors = {
        'MISSING_EXEC': MISSING_BINARY.format(binary='terraform-tools'),
    }
    super(TerraformToolsValidateOperation, self).__init__(
        binary='terraform-tools',
        check_hidden=True,
        # Install will be handled by the conversion operation
        install_if_missing=False,
        custom_errors=custom_errors,
        **kwargs)

  def _ParseArgsForCommand(self, command, input_file, policy_library, verbosity,
                           **kwargs):
    args = [
        command,
        input_file,
        '--verbosity',
        verbosity,
        '--policy-library',
        os.path.expanduser(policy_library),
    ]
    return args


@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA)
class Vet(base.Command):
  """Validate that a terraform plan complies with policies."""

  detailed_help = {
      'EXAMPLES':
          """
        To validate that a terraform plan complies with a policy library
        at `/my/policy/library`:

        $ {command} tfplan.json --policy-library=/my/policy/library
        """,
  }

  @staticmethod
  def Args(parser):
    parser.add_argument(
        'terraform_plan_json',
        help=(
            'File which contains a JSON export of a terraform plan. This file '
            'will be validated against the given policy library.'),
    )
    parser.add_argument(
        '--policy-library',
        required=True,
        help='Directory which contains a policy library',
    )
    parser.add_argument(
        '--zone',
        required=False,
        help='Default zone to use for resources that do not have one set',
    )
    parser.add_argument(
        '--region',
        required=False,
        help='Default region to use for resources that do not have one set',
    )

  def Run(self, args):
    tfplan_to_cai_operation = TerraformToolsTfplanToCaiOperation()
    validate_cai_operation = TerraformToolsValidateOperation()
    validate_tfplan_operation = TerraformToolsValidateOperation()

    env_vars = {
        'GOOGLE_OAUTH_ACCESS_TOKEN':
            GetFreshAccessToken(account=properties.VALUES.core.account.Get()),
        'USE_STRUCTURED_LOGGING':
            'true',
    }

    proxy_env_names = [
        'HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy', 'NO_PROXY',
        'no_proxy'
    ]

    # env names and orders are from
    # https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#full-reference
    project_env_names = [
        'GOOGLE_PROJECT',
        'GOOGLE_CLOUD_PROJECT',
        'GCLOUD_PROJECT',
    ]

    zone_env_names = [
        'GOOGLE_ZONE',
        'GCLOUD_ZONE',
        'CLOUDSDK_COMPUTE_ZONE',
    ]

    region_env_names = [
        'GOOGLE_REGION',
        'GCLOUD_REGION',
        'CLOUDSDK_COMPUTE_REGION',
    ]

    for env_key, env_val in os.environ.items():
      if env_key in proxy_env_names:
        env_vars[env_key] = env_val

    with files.TemporaryDirectory() as tempdir:
      cai_assets = os.path.join(tempdir, 'cai_assets.json')

      # project flag and CLOUDSDK_CORE_PROJECT env are linked with core property
      project = properties.VALUES.core.project.Get()
      if project:
        log.debug('Setting project to {} from properties'.format(project))
      else:
        for env_key in project_env_names:
          project = encoding.GetEncodedValue(os.environ, env_key)
          if project:
            log.debug('Setting project to {} from env {}'.format(
                project, env_key))
            break

      region = ''
      if args.region:
        region = args.region
        log.debug('Setting region to {} from args'.format(region))
      else:
        for env_key in region_env_names:
          region = encoding.GetEncodedValue(os.environ, env_key)
          if region:
            log.debug('Setting region to {} from env {}'.format(
                region, env_key))
            break

      zone = ''
      if args.zone:
        zone = args.zone
        log.debug('Setting zone to {} from args'.format(zone))
      else:
        for env_key in zone_env_names:
          zone = encoding.GetEncodedValue(os.environ, env_key)
          if zone:
            log.debug('Setting zone to {} from env {}'.format(zone, env_key))
            break

      response = tfplan_to_cai_operation(
          command='tfplan-to-cai',
          project=project,
          region=region,
          zone=zone,
          terraform_plan_json=args.terraform_plan_json,
          verbosity=args.verbosity,
          output_path=cai_assets,
          env=env_vars)
      self.exit_code = response.exit_code
      if self.exit_code > 0:
        # The streaming binary backed operation handles its own writing to
        # stdout and stderr, so there's nothing left to do here.
        return None

      with progress_tracker.ProgressTracker(
          message='Validating resources',
          aborted_message='Aborted validation.'):
        cai_response = validate_cai_operation(
            command='validate-cai',
            policy_library=args.policy_library,
            input_file=cai_assets,
            verbosity=args.verbosity,
            env=env_vars)
        tfplan_response = validate_tfplan_operation(
            command='validate-tfplan',
            policy_library=args.policy_library,
            input_file=args.terraform_plan_json,
            verbosity=args.verbosity,
            env=env_vars)

    # exit code 2 from a validate_* command indicates violations; we need to
    # pass that through to users so they can detect this case. However, if
    # either command errors out (exit code 1) return that instead.
    if cai_response.exit_code == 1 or tfplan_response.exit_code == 1:
      self.exit_code = 1
    elif cai_response.exit_code == 2 or tfplan_response.exit_code == 2:
      self.exit_code = 2

    # Output from validate commands uses "structured output", same as the
    # streaming output from conversion. The final output should be a combined
    # list of violations.
    violations = []

    for policy_type, response in (('CAI', cai_response), ('Terraform',
                                                          tfplan_response)):
      if response.stdout:
        try:
          msg = binary_operations.ReadStructuredOutput(
              response.stdout, as_json=True)
        except binary_operations.StructuredOutputError:
          log.warning('Could not parse {} policy validation output.'.format(
              policy_type))
        else:
          violations += msg.resource_body
      if response.stderr:
        handler = binary_operations.DefaultStreamStructuredErrHandler(None)
        for line in response.stderr.split('\n'):
          handler(line)

    return violations
