# -*- 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 insights 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 insight
from googlecloudsdk.api_lib.recommender import insight_types
from googlecloudsdk.api_lib.recommender import locations
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': """
        To list all insights for a billing account:

          $ {command} --project=project-id --location=global --insight-type=google.compute.firewall.Insight
        """,
}

DISPLAY_FORMAT = """
        table(
          name.basename(): label=INSIGHT_ID,
          category: label=CATEGORY,
          stateInfo.state: label=INSIGHT_STATE,
          lastRefreshTime: label=LAST_REFRESH_TIME,
          severity: label=SEVERITY,
          insightSubtype: label=INSIGHT_SUBTYPE,
          description: label=DESCRIPTION
        )
    """


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

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

  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 insights for. If no location is specified,'
            ' insights for all supported locations are listed.'
            ' Not specifying a location can add 15-20 seconds to the runtime.'
        ),
    )
    parser.add_argument(
        '--insight-type',
        metavar='INSIGHT_TYPE',
        required=False,
        help=(
            'Insight type to list insights for. If no insight type is'
            ' specified, insights for all supported insight types are listed.'
            ' Supported insight types can be found here: '
            'https://cloud.google.com/recommender/docs/insights/insight-types'
            ' Not specifying an insight-type can add a minute to the runtime.'
        ),
    )
    parser.add_argument(
        '--recursive',
        action=arg_parsers.StoreTrueFalseAction,
        required=False,
        help=("""
            In addition to listing the insights for the specified organization
            or folder, recursively list all of
            the insights 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 insights associated with that
            organization, all of the insights associated with that
            organization's folders and sub-folders, and all of the
            insights 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'
            ' insights 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)
    elif 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 ListInsightTypes(self, args):
    """List all Insight Types.

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

    Returns:
      (list) all insight-types in a list of strings
    """

    insighttypes_client = insight_types.CreateClient(self.ReleaseTrack())
    insighttypes_response = insighttypes_client.List(args.page_size)
    return [response.name for response in insighttypes_response]

  def GetInsights(self, args, asset_insight_types):
    """Collects all insights for a given Google Cloud Resource.

    Args:
      args: argparse.Namespace, The arguments that this command was invoked
        with.
      asset_insight_types: list, The list of Google Cloud resource insight types
        URL to collect insights

    Returns:
      (Insights) a iterator for all returned insights
    """
    insights = []
    insights_client = insight.CreateClient(self.ReleaseTrack())
    resource_prev = None
    location_prev = None
    for resource, location, insight_type in asset_insight_types:
      if resource != resource_prev or location != location_prev:
        log.status.Print(f'Reading Insights for: {resource} {location}')
      resource_prev = resource
      location_prev = location
      parent_name = '/'.join([resource, location, insight_type])
      new_insights = insights_client.List(
          parent_name, args.page_size
      )
      try:  # skip insight types that the user does not have access to.
        peek = next(new_insights)  # execute first element of generator
      except (
          apitools_exceptions.HttpBadRequestError,
          apitools_exceptions.BadStatusCodeError,
          StopIteration,
      ):
        continue
      insights = itertools.chain(
          insights, (peek,), new_insights
      )

    return insights

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

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

    Returns:
      The list of insights for this Google Cloud Resource.
    """
    if args.location and args.insight_type:
      insights_client = insight.CreateClient(self.ReleaseTrack())
      parent_name = flags.GetInsightTypeName(args)
      return insights_client.List(parent_name, args.page_size)

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

    # collect insights for all insight types
    asset_insight_types = []
    for asset in resource_locations:
      tokens = asset.split('/')
      resource = '/'.join(tokens[:2])
      location = '/'.join(tokens[2:4])
      if args.insight_type is not None:
        asset_insight_types.append(
            (resource, location, f'insightTypes/{args.insight_type}')
        )
      else:  # loop through all insight types
        asset_insight_types.extend([
            (resource, location, f'insightTypes/{name}')
            for name in self.ListInsightTypes(args)
        ])

    return self.GetInsights(args, asset_insight_types)


@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.GA)
class ListOriginal(base.ListCommand):
  r"""List insights for a Google Cloud entity.

  This command lists all insights for a given Google Cloud entity, location, and
  insight type. Supported insight-types can be found here:
  https://cloud.google.com/recommender/docs/insights/insight-types. Currently
  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 insights for. ',
    )
    parser.add_argument(
        '--insight-type',
        metavar='INSIGHT_TYPE',
        required=True,
        help=(
            'Insight type to list insights for. Supported insight-types can '
            'be found here: '
            'https://cloud.google.com/recommender/docs/insights/insight-types'
        ),
    )
    parser.display_info.AddFormat("""
        table(
          name.basename(): label=INSIGHT_ID,
          category: label=CATEGORY,
          stateInfo.state: label=INSIGHT_STATE,
          lastRefreshTime: label=LAST_REFRESH_TIME,
          severity: label=SEVERITY,
          insightSubtype: label=INSIGHT_SUBTYPE,
          description: label=DESCRIPTION
        )
    """)

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

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

    Returns:
      The list of insights for this project.
    """

    insights_client = insight.CreateClient(self.ReleaseTrack())
    parent_name = flags.GetInsightTypeName(args)

    return insights_client.List(parent_name, args.page_size)
