# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Cloud Backup Plans client."""


import collections

from googlecloudsdk.api_lib.backupdr import util
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.backupdr import util as command_util
from googlecloudsdk.core import properties


# TODO: b/416214401 - Add type annotations.


class BackupPlansClient(util.BackupDrClientBase):
  """Cloud Backup Plans client."""

  def __init__(self, api_version=util.DEFAULT_API_VERSION):
    super(BackupPlansClient, self).__init__(api_version=api_version)
    self.service = self.client.projects_locations_backupPlans

  def _ParseBackupRules(self, backup_rules):
    backup_rules_message = []
    if backup_rules is None:
      return []
    for backup_rule in backup_rules:
      standard_schedule = self.messages.StandardSchedule()
      standard_schedule.timeZone = (
          'UTC' if 'time-zone' not in backup_rule else backup_rule['time-zone']
      )
      standard_schedule.backupWindow = self.messages.BackupWindow(
          startHourOfDay=backup_rule['backup-window-start'],
          endHourOfDay=backup_rule['backup-window-end'],
      )
      standard_schedule.recurrenceType = (
          self.messages.StandardSchedule.RecurrenceTypeValueValuesEnum(
              backup_rule['recurrence']
          )
      )
      if 'hourly-frequency' in backup_rule:
        standard_schedule.hourlyFrequency = backup_rule['hourly-frequency']
      if 'days-of-week' in backup_rule:
        standard_schedule.daysOfWeek = [
            self.messages.StandardSchedule.DaysOfWeekValueListEntryValuesEnum(
                day
            )
            for day in backup_rule['days-of-week']
        ]
      if 'week-day-of-month' in backup_rule:
        week_day_of_month = backup_rule['week-day-of-month'].split('-')
        standard_schedule.weekDayOfMonth = self.messages.WeekDayOfMonth(
            weekOfMonth=self.messages.WeekDayOfMonth.WeekOfMonthValueValuesEnum(
                week_day_of_month[0]
            ),
            dayOfWeek=self.messages.WeekDayOfMonth.DayOfWeekValueValuesEnum(
                week_day_of_month[1]
            ),
        )
      if 'days-of-month' in backup_rule:
        standard_schedule.daysOfMonth = backup_rule['days-of-month']
      if 'months' in backup_rule:
        standard_schedule.months = [
            self.messages.StandardSchedule.MonthsValueListEntryValuesEnum(month)
            for month in backup_rule['months']
        ]
      backup_rule_message = self.messages.BackupRule(
          ruleId=backup_rule['rule-id'],
          backupRetentionDays=backup_rule['retention-days'],
          standardSchedule=standard_schedule,
      )
      backup_rules_message.append(backup_rule_message)
    return backup_rules_message

  def Create(
      self,
      resource,
      backup_vault,
      resource_type,
      backup_rules,
      log_retention_days,
      description,
      labels,
      max_custom_on_demand_retention_days,
      disk_properties=None,
      compute_instance_properties=None,
  ):
    """Creates a Backup Plan.

    Args:
      resource: The Backup Plan resource.
      backup_vault: The Backup Vault resource.
      resource_type: The resource type of the Backup Plan.
      backup_rules: The backup rules of the Backup Plan.
      log_retention_days: The log retention days of the Backup Plan.
      description: The description of the Backup Plan.
      labels: The labels of the Backup Plan.
      max_custom_on_demand_retention_days: The custom on demand retention days
        limit of the Backup Plan.
      disk_properties: A dictionary containing the disk properties of the
        Backup Plan. Expected keys include:
        'guest-flush': bool, Whether to enable guest flush.
      compute_instance_properties: A dictionary containing the compute instance
        properties of the Backup Plan. Expected keys include:
        'guest-flush': bool, Whether to enable guest flush.

    Returns:
      The created Backup Plan.
    """
    parent = resource.Parent().RelativeName()
    backup_plan_id = resource.Name()
    labels_message = None
    if labels:
      labels_message = self.messages.BackupPlan.LabelsValue(
          additionalProperties=[
              self.messages.BackupPlan.LabelsValue.AdditionalProperty(
                  key=key, value=value
              )
              for key, value in labels.items()
          ]
      )
    disk_props_message = None
    if disk_properties:
      disk_props_message = self.messages.DiskBackupPlanProperties(
          guestFlush=disk_properties.get('guest-flush', False)
      )

    compute_instance_props_message = None
    if compute_instance_properties:
      compute_instance_props_message = (
          self.messages.ComputeInstanceBackupPlanProperties(
              guestFlush=compute_instance_properties.get('guest-flush', False)
          )
      )

    backup_plan = self.messages.BackupPlan(
        resourceType=resource_type,
        backupVault=backup_vault,
        description=description,
        labels=labels_message,
        backupRules=self._ParseBackupRules(backup_rules),
        logRetentionDays=log_retention_days,
        maxCustomOnDemandRetentionDays=(
            int(max_custom_on_demand_retention_days)
            if max_custom_on_demand_retention_days is not None
            else None
        ),
    )
    if disk_props_message and (
        resource_type.endswith('/Disk')
        or resource_type
        == f'compute.{properties.VALUES.core.universe_domain.Get()}/Disk'
    ):
      backup_plan.diskBackupPlanProperties = disk_props_message
    elif compute_instance_props_message and (
        resource_type.endswith('/Instance')
        or resource_type
        == f'compute.{properties.VALUES.core.universe_domain.Get()}/Instance'
    ):
      backup_plan.computeInstanceBackupPlanProperties = (
          compute_instance_props_message
      )

    request = self.messages.BackupdrProjectsLocationsBackupPlansCreateRequest(
        parent=parent,
        backupPlan=backup_plan,
        backupPlanId=backup_plan_id,
    )
    return self.service.Create(request)

  def Describe(self, resource):
    """Describes a Backup Plan.

    Args:
      resource: The Backup Plan resource.

    Returns:
      The described Backup Plan.
    """
    request = self.messages.BackupdrProjectsLocationsBackupPlansGetRequest(
        name=resource.RelativeName()
    )
    return self.service.Get(request)

  def ParseUpdate(
      self,
      description,
      new_backup_rules_from_file,
      update_backup_rules,
      add_backup_rules,
      remove_backup_rules,
      current_backup_plan,
      log_retention_days,
      max_custom_on_demand_retention_days,
      disk_properties=None,
      compute_instance_properties=None,
  ):
    """Parses the update request for a Backup Plan.

    Args:
      description: The description of the Backup Plan.
      new_backup_rules_from_file: The backup rules to update from file in the
        Backup Plan.
      update_backup_rules: The backup rules to update in the Backup Plan.
      add_backup_rules: The backup rules to add to the Backup Plan.
      remove_backup_rules: The backup rules to remove from the Backup Plan.
      current_backup_plan: The current Backup Plan.
      log_retention_days: The log retention days of the Backup Plan.
      max_custom_on_demand_retention_days: The custom on demand retention days
        limit of the Backup Plan.
      disk_properties: A dictionary containing the disk properties of the
        Backup Plan.
      compute_instance_properties: A dictionary containing the compute instance
        properties of the Backup Plan.

    Returns:
      The updated Backup Plan.

    Raises:
      InvalidArgumentException: If the backup rules are invalid.
      ValueError: If the backup plan is not found.
    """
    if current_backup_plan is None:
      raise ValueError('Could not find the backup plan.')
    updated_backup_plan = self.messages.BackupPlan(
        resourceType=current_backup_plan.resourceType
    )
    if description is not None:
      updated_backup_plan.description = description
    if log_retention_days is not None:
      updated_backup_plan.logRetentionDays = log_retention_days
    if max_custom_on_demand_retention_days is not None:
      updated_backup_plan.maxCustomOnDemandRetentionDays = (
          int(max_custom_on_demand_retention_days)
      )
    current_rule_ids = {rule.ruleId for rule in current_backup_plan.backupRules}
    if new_backup_rules_from_file is not None:
      updated_backup_plan.backupRules = self._ParseBackupRules(
          new_backup_rules_from_file
      )
      return updated_backup_plan
    if update_backup_rules is not None:
      rule_ids = collections.Counter(
          [rule['rule-id'] for rule in update_backup_rules]
      )
      duplicate_rule_ids = [
          rule_id for rule_id, count in rule_ids.items() if count > 1
      ]
      if duplicate_rule_ids:
        raise exceptions.InvalidArgumentException(
            'rule-id',
            f'Rules {duplicate_rule_ids} found in more than one'
            ' --backup-rule flag.',
        )
      not_found_rule_ids = list(set([
          rule['rule-id']
          for rule in update_backup_rules
          if rule['rule-id'] not in current_rule_ids
      ]))
      if not_found_rule_ids:
        raise exceptions.InvalidArgumentException(
            'rule-id',
            f'Rules {not_found_rule_ids} not found in the backup plan.'
            ' The --backup-rule flag can only be used to modify existing'
            ' rules.',
        )
      update_rule_ids = [rule['rule-id'] for rule in update_backup_rules]
      updated_backup_plan.backupRules = [
          rule
          for rule in current_backup_plan.backupRules
          if rule.ruleId not in update_rule_ids
      ]
      updated_backup_plan.backupRules.extend(
          self._ParseBackupRules(update_backup_rules)
      )
    else:
      updated_backup_plan.backupRules = current_backup_plan.backupRules
    if add_backup_rules is not None:
      updated_backup_plan.backupRules.extend(
          self._ParseBackupRules(add_backup_rules)
      )
    if remove_backup_rules is not None:
      not_found_rule_ids = list(
          set(
              rule_id
              for rule_id in remove_backup_rules
              if rule_id not in current_rule_ids
          )
      )
      if not_found_rule_ids:
        raise exceptions.InvalidArgumentException(
            'rule-id',
            f'Rules {not_found_rule_ids} not found in the backup plan.',
        )
      updated_backup_plan.backupRules = [
          rule
          for rule in updated_backup_plan.backupRules
          if rule.ruleId not in remove_backup_rules
      ]
    if disk_properties is not None:
      updated_backup_plan.diskBackupPlanProperties = (
          self.messages.DiskBackupPlanProperties(
              guestFlush=disk_properties.get('guest-flush')
          )
      )
    if compute_instance_properties is not None:
      updated_backup_plan.computeInstanceBackupPlanProperties = (
          self.messages.ComputeInstanceBackupPlanProperties(
              guestFlush=compute_instance_properties.get('guest-flush')
          )
      )
    return updated_backup_plan

  def Update(self, resource, backup_plan, update_mask):
    """Updates a Backup Plan.

    Args:
      resource: The Backup Plan resource.
      backup_plan: The updated Backup Plan.
      update_mask: The update mask to edit the Backup Plan.

    Returns:
      The updated Backup Plan.
    """
    request_id = command_util.GenerateRequestId()
    request = self.messages.BackupdrProjectsLocationsBackupPlansPatchRequest(
        backupPlan=backup_plan,
        name=resource.RelativeName(),
        requestId=request_id,
        updateMask=update_mask,
    )
    return self.service.Patch(request)

  def Delete(self, resource):
    """Deletes a Backup Plan.

    Args:
      resource: The Backup Plan resource.

    Returns:
      The deleted Backup Plan.
    """
    request = self.messages.BackupdrProjectsLocationsBackupPlansDeleteRequest(
        name=resource.RelativeName()
    )
    return self.service.Delete(request)
