# -*- 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.

"""Command to show Cluster Ugprade Feature information for a Fleet."""


import re

import frozendict
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet.clusterupgrade import flags as clusterupgrade_flags
from googlecloudsdk.command_lib.container.fleet.features import base as feature_base
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.util import times


CLUSTER_UPGRADE_FEATURE = 'clusterupgrade'


@base.ReleaseTracks(
    base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA, base.ReleaseTrack.GA
)
class Describe(feature_base.DescribeCommand):
  """Describe the clusterupgrade feature for a fleet within a given project."""

  detailed_help = frozendict.frozendict({
      'DESCRIPTION': """\
          Describe the Fleet clusterupgrade feature used for configuring
          fleet-based rollout sequencing.
          """,
      'EXAMPLES': """\
          To view the cluster upgrade feature information for the current fleet, run:

              $ {command}
          """,
  })

  feature_name = CLUSTER_UPGRADE_FEATURE

  @staticmethod
  def Args(parser):
    flags = clusterupgrade_flags.ClusterUpgradeFlags(parser)
    flags.AddShowLinkedClusterUpgrade()

  def Run(self, args):
    project = arg_utils.GetFromNamespace(args, '--project', use_defaults=True)
    feature = self.GetFeature(project=project)
    return self.GetFleetClusterUpgradeInfo(project, feature, args)

  @staticmethod
  def GetProjectIDFromFleet(fleet):
    """Extracts the project ID from the fleet."""
    return fleet

  @staticmethod
  def FormatDurations(cluster_upgrade_spec):
    """Formats display strings for all cluster upgrade duration fields."""
    if cluster_upgrade_spec.postConditions is not None:
      default_soaking = cluster_upgrade_spec.postConditions.soaking
      if default_soaking is not None:
        cluster_upgrade_spec.postConditions.soaking = Describe.DisplayDuration(
            default_soaking
        )
    for override in cluster_upgrade_spec.gkeUpgradeOverrides:
      if override.postConditions is not None:
        override_soaking = override.postConditions.soaking
        if override_soaking is not None:
          override.postConditions.soaking = Describe.DisplayDuration(
              override_soaking
          )
    return cluster_upgrade_spec

  @staticmethod
  def DisplayDuration(proto_duration_string):
    """Returns the display string for a duration value."""
    duration = times.ParseDuration(proto_duration_string)
    iso_duration = times.FormatDuration(duration)
    return re.sub('[-PT]', '', iso_duration).lower()

  def GetFleetClusterUpgradeInfo(self, fleet, feature, args):
    """Gets Cluster Upgrade Feature information for the provided Fleet."""
    if (
        args.IsKnownAndSpecified('show_linked_cluster_upgrade')
        and args.show_linked_cluster_upgrade
    ):
      return self.GetLinkedClusterUpgrades(fleet, feature)
    return Describe.GetClusterUpgradeInfo(fleet, feature)

  @staticmethod
  def GetClusterUpgradeInfo(fleet, feature):
    """Gets Cluster Upgrade Feature information for the provided Fleet."""
    fleet_spec = feature.spec.clusterupgrade
    if not fleet_spec:
      msg = ('Cluster Upgrade feature is not configured for Fleet: {}.').format(
          fleet
      )
      raise exceptions.Error(msg)

    res = {
        'fleet': fleet,
        'spec': Describe.FormatDurations(fleet_spec),
    }
    if feature.state is not None and feature.state.clusterupgrade is not None:
      res['state'] = feature.state.clusterupgrade
    return res

  def GetLinkedClusterUpgrades(self, fleet, feature):
    """Gets Cluster Upgrade Feature information for the entire sequence."""

    current_project = Describe.GetProjectIDFromFleet(fleet)
    visited = set([fleet])

    def _UpTheStream(cluster_upgrade):
      """Recursively gets information for the upstream Fleets."""
      upstream_spec = cluster_upgrade.get('spec', None)
      upstream_fleets = upstream_spec.upstreamFleets if upstream_spec else None
      if not upstream_fleets:
        return [cluster_upgrade]

      # Currently, we only process the first upstream Fleet in the
      # Cluster Upgrade Feature, forming a linked-list of Fleets. If the API
      # ever supports multiple upstream Fleets (i.e., graph of Fleets), this
      # will need to be modified to recurse on every Fleet.
      upstream_fleet = upstream_fleets[0]
      if upstream_fleet in visited:
        return [cluster_upgrade]  # Detected a cycle.
      visited.add(upstream_fleet)

      upstream_fleet_project = Describe.GetProjectIDFromFleet(upstream_fleet)
      upstream_feature = (
          feature
          if upstream_fleet_project == current_project
          else self.GetFeature(project=upstream_fleet_project)
      )
      try:
        upstream_cluster_upgrade = Describe.GetClusterUpgradeInfo(
            upstream_fleet, upstream_feature
        )
      except exceptions.Error as e:
        log.warning(e)
        return [cluster_upgrade]
      return _UpTheStream(upstream_cluster_upgrade) + [cluster_upgrade]

    def _DownTheStream(cluster_upgrade):
      """Recursively gets information for the downstream Fleets."""
      downstream_state = cluster_upgrade.get('state', None)
      downstream_fleets = (
          downstream_state.downstreamFleets if downstream_state else None
      )
      if not downstream_fleets:
        return [cluster_upgrade]

      # Currently, we only process the first downstream Fleet in the
      # Cluster Upgrade Feature, forming a linked-list of Fleet. If the API
      # ever supports multiple downstream Fleets (i.e., graph of Fleets), this
      # will need to be modified to recurse on every Scope.
      downstream_fleet = downstream_fleets[0]
      if downstream_fleet in visited:
        return [cluster_upgrade]  # Detected a cycle.
      visited.add(downstream_fleet)

      downstream_scope_project = Describe.GetProjectIDFromFleet(
          downstream_fleet
      )
      downstream_feature = (
          feature
          if downstream_scope_project == current_project
          else self.GetFeature(project=downstream_scope_project)
      )
      downstream_cluster_upgrade = Describe.GetClusterUpgradeInfo(
          downstream_fleet, downstream_feature
      )
      return [cluster_upgrade] + _DownTheStream(downstream_cluster_upgrade)

    current_cluster_upgrade = Describe.GetClusterUpgradeInfo(fleet, feature)
    upstream_cluster_upgrades = _UpTheStream(current_cluster_upgrade)[:-1]
    downstream_cluster_upgrades = _DownTheStream(current_cluster_upgrade)[1:]
    return (
        upstream_cluster_upgrades
        + [current_cluster_upgrade]
        + downstream_cluster_upgrades
    )
