# -*- 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.
"""The command to get the status of Config Management Feature."""


from googlecloudsdk.api_lib.container.fleet import util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container.fleet import api_util
from googlecloudsdk.command_lib.container.fleet.config_management import utils
from googlecloudsdk.command_lib.container.fleet.features import base as feature_base
from googlecloudsdk.core import log


NA = 'NA'

DETAILED_HELP = {
    'EXAMPLES': """\
   Print the status of the Config Management feature:

    $ {command}

    Name            | Status | Last_Synced_Token | Sync_Branch | Last_Synced_Time              | Policy_Controller | Hierarchy_Controller | Version | Upgrades | Synced_To_Fleet_Default
    --------------- | ------ | ----------------- | ----------- | ----------------------------- | ----------------- | -------------------- | ------- | -------- | ----------------------------
    managed-cluster | SYNCED | 2945500b7f        | acme        | 2020-03-23 11:12:31 -0700 PDT | NA                | NA                   | 1.18.3  | auto     | FLEET_DEFAULT_NOT_CONFIGURED


  View the status for the cluster named `managed-cluster-a`:

    $ {command} --flatten=acm_status --filter="acm_status.name:managed-cluster-a"

  Use a regular expression to list status for multiple clusters:

    $ {command} --flatten=acm_status --filter="acm_status.name ~ managed-cluster.*"

  List all clusters where current Config Sync `Status` is `SYNCED`:

    $ {command} --flatten=acm_status --filter="acm_status.config_sync:SYNCED"

  List all the clusters where sync_branch is `v1` and current Config Sync
  `Status` is not `SYNCED`:

    $ {command} --flatten=acm_status --filter="acm_status.sync_branch:v1 AND -acm_status.config_sync:SYNCED"
  """,
}


class ConfigmanagementFeatureState(object):
  """Feature state class stores ACM status."""

  def __init__(self, cluster_name):
    self.name = cluster_name
    self.config_sync = NA
    self.last_synced_token = NA
    self.last_synced = NA
    self.sync_branch = NA
    self.policy_controller_state = NA
    self.hierarchy_controller_state = NA
    self.version = NA
    self.upgrades = NA
    self.synced_to_fleet_default = NA

  def update_sync_state(self, fs):
    """Update config_sync state for the membership that has ACM installed.

    Args:
      fs: ConfigManagementFeatureState
    """
    if (
        fs.configSyncState is None
        or fs.configSyncState.state.name != 'CONFIG_SYNC_INSTALLED'
    ):
      return

    if fs.configSyncState.syncState:
      if fs.configSyncState.syncState.syncToken:
        self.last_synced_token = fs.configSyncState.syncState.syncToken[:7]
      self.last_synced = fs.configSyncState.syncState.lastSyncTime
      if has_config_sync_git(fs):
        self.sync_branch = fs.membershipSpec.configSync.git.syncBranch

  def update_policy_controller_state(self, md):
    """Update policy controller state for the membership that has ACM installed.

    Args:
      md: MembershipFeatureState
    """
    # Also surface top-level Feature Authorizer errors.
    if md.state.code.name != 'OK':
      self.policy_controller_state = 'ERROR: {}'.format(md.state.description)
      return
    fs = md.configmanagement
    if not (
        fs.policyControllerState and fs.policyControllerState.deploymentState
    ):
      self.policy_controller_state = NA
      return
    pc_deployment_state = fs.policyControllerState.deploymentState
    expected_deploys = {
        'GatekeeperControllerManager': (
            pc_deployment_state.gatekeeperControllerManagerState
        )
    }
    if (
        fs.membershipSpec
        and fs.membershipSpec.version
        and fs.membershipSpec.version > '1.4.1'
    ):
      expected_deploys['GatekeeperAudit'] = pc_deployment_state.gatekeeperAudit
    for deployment_name, deployment_state in expected_deploys.items():
      if not deployment_state:
        continue
      elif deployment_state.name != 'INSTALLED':
        self.policy_controller_state = '{} {}'.format(
            deployment_name, deployment_state
        )
        return
      self.policy_controller_state = deployment_state.name

  def update_hierarchy_controller_state(self, fs):
    """Update hierarchy controller state for the membership that has ACM installed.

    The PENDING state is set separately after this logic. The PENDING state
    suggests the HC part in feature_spec and feature_state are inconsistent, but
    the HC status from feature_state is not ERROR. This suggests that HC might
    be still in the updating process, so we mark it as PENDING

    Args:
      fs: ConfigmanagementFeatureState
    """
    if not (fs.hierarchyControllerState and fs.hierarchyControllerState.state):
      self.hierarchy_controller_state = NA
      return
    hc_deployment_state = fs.hierarchyControllerState.state

    hnc_state = 'NOT_INSTALLED'
    ext_state = 'NOT_INSTALLED'
    if hc_deployment_state.hnc:
      hnc_state = hc_deployment_state.hnc.name
    if hc_deployment_state.extension:
      ext_state = hc_deployment_state.extension.name
    # partial mapping from ('hnc_state', 'ext_state') to 'HC_STATE',
    # ERROR, PENDING, NA states are identified separately
    deploys_to_status = {
        ('INSTALLED', 'INSTALLED'): 'INSTALLED',
        ('INSTALLED', 'NOT_INSTALLED'): 'INSTALLED',
        ('NOT_INSTALLED', 'NOT_INSTALLED'): NA,
    }
    if (hnc_state, ext_state) in deploys_to_status:
      self.hierarchy_controller_state = deploys_to_status[
          (hnc_state, ext_state)
      ]
    else:
      self.hierarchy_controller_state = 'ERROR'

  def update_pending_state(self, api, feature_spec_mc, feature_state_mc):
    """Update Config Management component states if spec does not match state.

    Args:
      api: GKE Hub API
      feature_spec_mc: MembershipConfig
      feature_state_mc: MembershipConfig
    """
    feature_state_pending = (
        feature_state_mc is None and feature_spec_mc is not None
    )
    if feature_state_pending:
      self.last_synced_token = utils.STATUS_PENDING
      self.last_synced = utils.STATUS_PENDING
      self.sync_branch = utils.STATUS_PENDING
      if self.config_sync == NA:
        self.config_sync = utils.STATUS_PENDING
    if (
        self.policy_controller_state.__str__()
        in ['INSTALLED', 'GatekeeperAudit NOT_INSTALLED', NA]
        and feature_state_pending
    ):
      self.policy_controller_state = utils.STATUS_PENDING

    hc_semantic_copy = (
        lambda hc_spec: hc_spec if hc_spec is not None
        else api.ConfigManagementHierarchyControllerConfig()
    )
    if (
        self.hierarchy_controller_state.__str__() != utils.STATUS_ERROR
        and feature_state_pending
        or hc_semantic_copy(feature_spec_mc.hierarchyController)
        != hc_semantic_copy(feature_state_mc.hierarchyController)
    ):
      self.hierarchy_controller_state = utils.STATUS_PENDING


@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA)
class Status(feature_base.FeatureCommand, base.ListCommand):
  """Print the status of all clusters with Config Management enabled.

  The `Status` column indicates the status of the Config Sync component.
  `Status` displays `NOT_INSTALLED` when Config Sync is not installed.
  `Status` displays `NOT_CONFIGURED` when Config Sync is installed but git/oci
  is not configured. `Status` displays `SYNCED` when Config Sync is installed
  and git/oci is configured and the last sync was successful. `Status` displays
  `ERROR` when Config Sync encounters errors. `Status` displays `STOPPED` when
  Config Sync stops syncing configs to the cluster. `Status` displays
  `PENDING` when Config Sync has not reached the desired state. Otherwise,
  `Status` displays `UNSPECIFIED`.

  The `Synced_to_Fleet_Default` status indicates whether each membership's
  configuration has been synced with the [fleet-default membership configuration
  ](https://cloud.google.com/kubernetes-engine/fleet-management/docs/manage-features).
  `Synced_to_Fleet_Default` displays `FLEET_DEFAULT_NOT_CONFIGURED` when
  fleet-default membership configuration is not enabled.
  `Synced_to_Fleet_Default` for an individual membership may be `UNKNOWN` if
  configuration has yet to be applied to this membership since enabling
  fleet-default membership configuration.
  See the `enable` and `apply` commands for more details.
  """

  detailed_help = DETAILED_HELP

  feature_name = 'configmanagement'

  @staticmethod
  def Args(parser):
    parser.display_info.AddFormat("""
    multi(acm_status:format='table(
            name:label=Name:sort=1,
            config_sync:label=Status,
            last_synced_token:label="Last_Synced_Token",
            sync_branch:label="Sync_Branch",
            last_synced:label="Last_Synced_Time",
            policy_controller_state:label="Policy_Controller",
            hierarchy_controller_state:label="Hierarchy_Controller",
            version:label="Version",
            upgrades:label="Upgrades",
            synced_to_fleet_default:label="Synced_to_Fleet_Default"
      )' , acm_errors:format=list)
    """)

  def Run(self, _):
    memberships, unreachable = api_util.ListMembershipsFull()
    if unreachable:
      log.warning(
          'Locations {} are currently unreachable. Status '
          'entries may be incomplete'.format(unreachable)
      )
    if not memberships:
      return None
    self.f = self.GetFeature()
    acm_status = []
    acm_errors = []

    self.feature_spec_memberships = {
        util.MembershipPartialName(m): s
        for m, s in self.hubclient.ToPyDict(self.f.membershipSpecs).items()
        if s is not None and s.configmanagement is not None
    }
    feature_state_memberships = {
        util.MembershipPartialName(m): s
        for m, s in self.hubclient.ToPyDict(self.f.membershipStates).items()
    }
    # TODO(b/298461043): Refactor this method so it is more readable.
    for name in memberships:
      name = util.MembershipPartialName(name)
      cluster = ConfigmanagementFeatureState(name)
      cluster.synced_to_fleet_default = self.fleet_default_sync_status(name)
      if name not in feature_state_memberships:
        if name in self.feature_spec_memberships:
          # (b/187846229) Show PENDING if feature spec is aware of
          # this membership name but feature state is not
          cluster.update_pending_state(
              self.messages,
              self.feature_spec_memberships[name],
              None
          )
        acm_status.append(cluster)
        continue
      md = feature_state_memberships[name]
      fs = md.configmanagement
      # (b/153587485) Show FeatureState.code if it's not OK
      # as it indicates an unreachable cluster or a dated syncState.code
      if md.state is None or md.state.code is None:
        cluster.config_sync = 'CODE_UNSPECIFIED'
      elif fs is None:
        cluster.config_sync = utils.STATUS_NOT_INSTALLED
      else:
        # operator errors could occur regardless of the deployment_state
        if has_operator_error(fs):
          append_error(name, fs.operatorState.errors, acm_errors)
        # We should update PoCo state regardless of operator state.
        cluster.update_policy_controller_state(md)
        if not has_operator_state(fs):
          if md.state.code.name != 'OK':
            cluster.config_sync = md.state.code.name
          else:
            cluster.config_sync = 'OPERATOR_STATE_UNSPECIFIED'
        else:
          # Set cluster.upgrades
          if (
              fs.membershipSpec is not None
              and fs.membershipSpec.management is not None
              and fs.membershipSpec.management.name
              == utils.MANAGEMENT_AUTOMATIC
          ):
            cluster.upgrades = utils.UPGRADES_AUTO
          else:
            cluster.upgrades = utils.UPGRADES_MANUAL

          # Set cluster.version
          if fs.membershipSpec is not None:
            cluster.version = fs.membershipSpec.version

          # Set cluster.config_sync
          if fs.configSyncState.state is not None:
            cluster.config_sync = config_sync_state(fs)

          # Set cluster.last_synced_token, cluster.sync_branch and
          # cluster.last_synced_time
          cluster.update_sync_state(fs)

          # Add errors into acm_errors
          if fs.configSyncState.errors:
            append_error(name, fs.configSyncState.errors, acm_errors)
          if has_config_sync_sync_error(fs):
            append_error(name, fs.configSyncState.syncState.errors, acm_errors)

          # Set cluster.hierarchy_controller_state
          cluster.update_hierarchy_controller_state(fs)

          if name in self.feature_spec_memberships:
            cluster.update_pending_state(
                self.messages,
                self.feature_spec_memberships[name].configmanagement,
                fs.membershipSpec,
            )
      acm_status.append(cluster)
    return {'acm_errors': acm_errors, 'acm_status': acm_status}

  def fleet_default_sync_status(self, membership):
    if not self.f.fleetDefaultMemberConfig:
      return 'FLEET_DEFAULT_NOT_CONFIGURED'
    if (membership not in self.feature_spec_memberships or
        self.feature_spec_memberships[membership].origin is None):
      return 'UNKNOWN'
    origin = self.feature_spec_memberships[membership].origin.type
    if origin == self.messages.Origin.TypeValueValuesEnum.FLEET:
      return 'YES'
    if (origin == self.messages.Origin.TypeValueValuesEnum.USER or
        origin == self.messages.Origin.TypeValueValuesEnum.FLEET_OUT_OF_SYNC):
      return 'NO'
    return 'UNKNOWN'


def config_sync_state(fs):
  """Convert state to a string shown to the users.

  Args:
    fs: ConfigManagementFeatureState

  Returns:
    a string shown to the users representing the Config Sync state.
  """

  if (
      fs.configSyncState is not None
      and fs.configSyncState.clusterLevelStopSyncingState is not None
  ):
    if fs.configSyncState.clusterLevelStopSyncingState.name in [
        utils.STATUS_STOPPED,
        utils.STATUS_PENDING,
    ]:
      return fs.configSyncState.clusterLevelStopSyncingState.name

  cs_installation_state = fs.configSyncState.state.name

  if cs_installation_state == 'CONFIG_SYNC_PENDING':
    return utils.STATUS_PENDING

  if cs_installation_state == 'CONFIG_SYNC_INSTALLED':
    if fs.configSyncState and fs.configSyncState.syncState:
      return fs.configSyncState.syncState.code.name
    return utils.STATUS_INSTALLED

  if cs_installation_state == 'CONFIG_SYNC_NOT_INSTALLED':
    return utils.STATUS_NOT_INSTALLED

  if cs_installation_state == 'CONFIG_SYNC_ERROR':
    return utils.STATUS_ERROR

  return 'UNSPECIFIED'


def has_operator_state(fs):
  return fs and fs.operatorState and fs.operatorState.deploymentState


def has_operator_error(fs):
  return fs and fs.operatorState and fs.operatorState.errors


def has_config_sync_sync_error(fs):
  return (
      fs
      and fs.configSyncState
      and fs.configSyncState.syncState
      and fs.configSyncState.syncState.errors
  )


def has_config_sync_git(fs):
  return (
      fs.membershipSpec
      and fs.membershipSpec.configSync
      and fs.membershipSpec.configSync.git
  )


def append_error(cluster, state_errors, acm_errors):
  for error in state_errors:
    acm_errors.append({'cluster': cluster, 'error': error.errorMessage})
