# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""recommender API recommendations list command."""


import itertools

from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.asset import client_util
from googlecloudsdk.api_lib.recommender import locations
from googlecloudsdk.api_lib.recommender import recommendation
from googlecloudsdk.api_lib.recommender import recommenders
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.recommender import flags
from googlecloudsdk.command_lib.run import exceptions
from googlecloudsdk.core import log


DETAILED_HELP = {
    'EXAMPLES':
        """
          Lists recommendations for a Cloud project.
            $ {command} --project=project-id --location=global --recommender=google.compute.instance.MachineTypeRecommender
        """,
}

DISPLAY_FORMAT = """
        table(
          name.basename(): label=RECOMMENDATION_ID,
          primaryImpact.category: label=PRIMARY_IMPACT_CATEGORY,
          stateInfo.state: label=RECOMMENDATION_STATE,
          lastRefreshTime: label=LAST_REFRESH_TIME,
          priority: label=PRIORITY,
          recommenderSubtype: label=RECOMMENDER_SUBTYPE,
          description: label=DESCRIPTION
        )
    """


@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA)
class List(base.ListCommand):
  r"""List recommendations for Google Cloud resources.

  This command lists all recommendations for the specified Google Cloud
  resource, location, and recommender. If a recommender or location is not
  specified, recommendations for all supported recommenders or locations,
  respectively, are listed. If the `--recursive` flag is set,
  recommendations for child resources and projects are also listed.
  Supported recommenders can be found here:
  https://cloud.google.com/recommender/docs/recommenders.
  """

  detailed_help = DETAILED_HELP

  @staticmethod
  def Args(parser):
    """Args is called by calliope to gather arguments for this command.

    Args:
      parser: An argparse parser that you can use to add arguments that go on
        the command line after this command.
    """
    flags.AddParentFlagsToParser(parser)
    parser.add_argument(
        '--location',
        metavar='LOCATION',
        required=False,
        help=(
            'Location to list recommendations for. If no location is specified,'
            ' recommendations for all supported locations are listed.'
            ' Not specifying a location can add 15-20 seconds to the runtime.'
        ),
    )
    parser.add_argument(
        '--recommender',
        metavar='RECOMMENDER',
        required=False,
        help=(
            'Recommender to list recommendations for. If no recommender is'
            ' specified, recommendations for all supported recommenders are'
            ' listed. Supported recommenders can be found here:'
            ' https://cloud.google.com/recommender/docs/recommenders'
            ' Not specifying a recommender can add 15-20 seconds to the'
            ' runtime.'
        ),
    )
    parser.add_argument(
        '--recursive',
        required=False,
        action=arg_parsers.StoreTrueFalseAction,
        help=("""
            In addition to listing the recommendations for the specified
            organization or folder, recursively list all of
            the recommendations for the resource's child resources, including
            their descendants (for example, a folder's sub-folders), and for the
            resource's child projects. For example, when using the
            `--recursive` flag and specifying an organization, the response
            lists all of the recommendations associated with that
            organization, all of the recommendations associated with that
            organization's folders and sub-folders, and all of the
            recommendations associated with that organization's child
            projects.  The maximum number of resources (organization,
            folders, projects, and descendant resources) that can be accessed at
            once with the `--recursive` flag is 100. For a larger
            number of nested resources, use
            [BigQuery export](https://cloud.google.com/recommender/docs/bq-export/export-recommendations-to-bq).
            Using `--recursive` can add 15-20 seconds per resource to the
            runtime.
            """),
    )
    parser.display_info.AddFormat(DISPLAY_FORMAT)

  def setArgs(self, args):
    """Setups up args to search all resources under a project, folder, or organization.

    Args:
      args: argparse.Namespace, The arguments that this command was invoked
        with.

    Returns:
      (argparse.Namespace) args with additional parameters setup
    """

    args.read_mask = '*'
    args.asset_types = [
        # gcloud-disable-gdu-domain
        'cloudresourcemanager.googleapis.com/Project',
        # gcloud-disable-gdu-domain
        'cloudresourcemanager.googleapis.com/Folder'
    ]
    args.order_by = 'createTime'
    args.query = '*'
    if args.project:
      args.scope = 'projects/' + args.project
    if args.organization:
      args.scope = 'organizations/' + args.organization
    if args.folder:
      args.scope = 'folders/' + args.folder

    return args

  def read(self, asset_in):
    if isinstance(asset_in, list):
      return asset_in[0]
    else:
      return asset_in

  def AddResource(self, resource_location) -> bool:
    if resource_location not in self.resource_locations:
      self.resource_locations.append(resource_location)
      return True
    return False

  def searchAllResources(self, args):
    """Search all nested resources under a project, folder, or organization.

    Args:
      args: argparse.Namespace, The arguments that this command was invoked
        with.

    Returns:
      (List): a list of all Google Cloud resource,location pairs
    """

    args = self.setArgs(args)
    client = client_util.AssetSearchClient(client_util.DEFAULT_API_VERSION)
    resources = list(client.SearchAllResources(args))
    self.resource_locations = []

    for r in resources:
      parent_resource = f'{self.read(args.scope)}/locations/{r.location}'
      if 'project' not in parent_resource:
        self.AddResource(parent_resource)

      # gcloud-disable-gdu-domain
      if r.assetType == 'cloudresourcemanager.googleapis.com/Project':
        self.AddResource(f'{self.read(r.project)}/locations/{r.location}')

      # gcloud-disable-gdu-domain
      if (
          r.assetType == 'cloudresourcemanager.googleapis.com/Folder'
          and self.AddResource(f'{self.read(r.folders)}/locations/{r.location}')
      ):
        args.scope = self.read(r.folders)
        resources.extend(client.SearchAllResources(args))
      if len(self.resource_locations) > 100:
        raise exceptions.UnsupportedOperationError(
            'The maximum number of resources (organizations, folders, projects,'
            ' and descendant resources) that can be accessed to list'
            ' recommendations is 100. To access'
            ' a larger number of resources, use BigQuery Export.'
        )
    return self.resource_locations

  def CollectAssets(self, args):
    """Iterate through search all resources response and collects unique Google Cloud resouce,location pairs.

    Args:
      args: argparse.Namespace, The arguments that this command was invoked
        with.

    Returns:
      (List): a list of all Google Cloud resource,location pairs
    """

    # Collect Assets and Locations
    log.status.Print('Collecting Resources... This may take some time...')
    if args.recursive:
      resource_locations = self.searchAllResources(args)
    else:
      if args.location is None:
        loc_client = locations.CreateClient(self.ReleaseTrack())
        resource_locations = [
            loc.name
            for loc in loc_client.List(
                args.page_size,
                project=args.project,
                organization=args.organization,
                folder=args.folder,
                billing_account=args.billing_account,
            )
        ]
      else:
        resource_locations = [
            flags.GetResourceSegment(args) + f'/locations/{args.location}'
        ]
    return resource_locations

  def ListRecommenders(self, args):
    """List all Recommenders.

    Args:
      args: argparse.Namespace, The arguments that this command was invoked
        with.

    Returns:
      (list) all recommenders in a list of strings
    """

    recommenders_client = recommenders.CreateClient(self.ReleaseTrack())
    recommenders_response = recommenders_client.List(args.page_size)
    return list(recommenders_response)

  def GetRecommendations(self, args, asset_recommenders):
    """Collects all recommendations for a given Google Cloud Resource.

    Args:
      args: argparse.Namespace, The arguments that this command was invoked
        with.
      asset_recommenders: list, The list of Google Cloud resource recommender
        URL to collect recommendations

    Returns:
      (Recommendations) a iterator for all returned recommendations
    """

    recommendations = []
    recommendations_client = recommendation.CreateClient(self.ReleaseTrack())

    resource_prev = None
    location_prev = None
    for resource, location, recommender in asset_recommenders:
      if resource != resource_prev or location != location_prev:
        log.status.Print(f'Reading Recommendations for: {resource} {location}')
      resource_prev = resource
      location_prev = location
      parent_name = '/'.join([resource, location, recommender])
      new_recommendations = recommendations_client.List(
          parent_name, args.page_size
      )
      try:  # skip recommenders that the user does not have access to.
        peek = next(new_recommendations)  # execute first element of generator
      except (
          apitools_exceptions.HttpBadRequestError,
          apitools_exceptions.BadStatusCodeError,
          StopIteration,
      ):
        continue
      recommendations = itertools.chain(
          recommendations, (peek,), new_recommendations
      )

    return recommendations

  def Run(self, args):
    """Run 'gcloud recommender recommendations list'.

    Args:
      args: argparse.Namespace, The arguments that this command was invoked
        with.

    Returns:
      The list of recommendations for this project.
    """
    if args.location and args.recommender:
      recommendations_client = recommendation.CreateClient(self.ReleaseTrack())
      parent_name = flags.GetRecommenderName(args)
      return recommendations_client.List(parent_name, args.page_size)

    # Collect Assets and Locations
    resource_locations = self.CollectAssets(args)

    # collect recommendations for all recommenders
    asset_recommenders = []
    for asset in resource_locations:
      tokens = asset.split('/')
      resource = '/'.join(tokens[:2])
      location = '/'.join(tokens[2:4])
      if args.recommender is not None:
        asset_recommenders.append(
            (resource, location, f'recommenders/{args.recommender}')
        )
      else:  # loop through all recommenders
        asset_recommenders.extend([
            (resource, location, f'recommenders/{response.name}')
            for response in self.ListRecommenders(args)
        ])

    return self.GetRecommendations(args, asset_recommenders)


@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.GA)
class ListOriginal(base.ListCommand):
  r"""List operations for a recommendation.

  This command lists all recommendations for a given Google Cloud entity ID,
  location, and recommender. Supported recommenders can be found here:
  https://cloud.google.com/recommender/docs/recommenders.
  The following Google Cloud entity types are supported: project,
  billing_account, folder and organization.
  """

  detailed_help = DETAILED_HELP

  @staticmethod
  def Args(parser):
    """Args is called by calliope to gather arguments for this command.

    Args:
      parser: An argparse parser that you can use to add arguments that go on
        the command line after this command.
    """
    flags.AddParentFlagsToParser(parser)
    parser.add_argument(
        '--location',
        metavar='LOCATION',
        required=True,
        help='Location to list recommendations for.',
    )
    parser.add_argument(
        '--recommender',
        metavar='RECOMMENDER',
        required=True,
        help=(
            'Recommender to list recommendations for. Supported recommenders'
            ' can be found here:'
            ' https://cloud.google.com/recommender/docs/recommenders.'
        ),
    )
    parser.display_info.AddFormat(DISPLAY_FORMAT)

  def Run(self, args):
    """Run 'gcloud recommender recommendations list'.

    Args:
      args: argparse.Namespace, The arguments that this command was invoked
        with.

    Returns:
      The list of recommendations for this project.
    """
    recommendations_client = recommendation.CreateClient(self.ReleaseTrack())
    parent_name = flags.GetRecommenderName(args)

    return recommendations_client.List(parent_name, args.page_size)
