# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""List incompatible features for GAE to CR migration."""
import logging
from os import path as os_path
from typing import Dict, List, Mapping, Sequence
from apitools.base.protorpclite import messages
from apitools.base.py import encoding
from googlecloudsdk.command_lib.app.gae_to_cr_migration_util.common import util
from googlecloudsdk.command_lib.app.gae_to_cr_migration_util.config import feature_helper
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import properties
from googlecloudsdk.core import yaml


_TEMPLATE_PATH = os_path.join(os_path.dirname(__file__), '../config/')


class IncompatibleFeaturesFoundError(exceptions.Error):
  """An error that is raised when incompatible features are found."""


def check_for_urlmap_conditions(
    url_maps: List[any], input_type: feature_helper.InputType
) -> bool:
  """Checks if any UrlMap in the list has urlRegex='.*' and scriptPath='auto'.

  Args:
      url_maps: A list of UrlMap objects.
      input_type: The input type of the app.yaml file.
  Returns:
      True if all UrlMap matches the conditions, False otherwise.
  """
  for url_map in url_maps:
    if (
        input_type == feature_helper.InputType.ADMIN_API
        and (
            url_map.get('urlRegex') == '.*' or url_map.get('urlRegex') == '/.*'
        )
        and url_map.get('script', {}).get('scriptPath')
        in ['auto', 'PLACEHOLDER']
    ):
      continue
    elif (
        input_type == feature_helper.InputType.APP_YAML
        and (url_map.get('url') == '.*' or url_map.get('url') == '/.*')
        and url_map.get('script') in ['auto', 'PLACEHOLDER']
    ):
      continue
    else:
      return False
  return True


def get_length(val: any) -> int:
  """Returns the length of the given value."""
  if isinstance(val, list):
    return len(val)
  elif isinstance(val, str):
    return len(val)
  elif isinstance(val, bytes):
    return len(val)
  else:
    return 0


def list_incompatible_features(
    input_data: Mapping[str, any],
    input_type: feature_helper.InputType,
    appyaml: str,
    service: str,
    version: str,
) -> None:
  """Lists incompatible features for GAE to Cloud Run migration.

  This function checks for incompatible features in either a local app.yaml file
  or a deployed App Engine version.

  Args:
      input_data: The input data, containing Message objects.
      input_type: The input type of the input data.
      appyaml: The path to the app.yaml file.
      service: The service name.
      version: The version name.
  """
  if not input_type or not input_data:
    return
  incompatible_list = _check_for_incompatibility(input_data, input_type)
  input_name = _generate_input_name(input_type, appyaml, service, version)
  _generate_output(incompatible_list, input_type, input_name)


def _generate_input_name(
    input_type: feature_helper.InputType,
    appyaml: str,
    service: str,
    version: str,
) -> str:
  """Generates the input name for the input type."""
  if input_type == feature_helper.InputType.APP_YAML:
    return appyaml
  project_id = properties.VALUES.core.project.Get()
  return f'{project_id}/{service}/{version}'


def _convert_message_to_dict(data: Mapping[str, any]):
  """Recursively converts apitools messages.Message objects to dictionaries.

  Args:
      data: The input data, potentially containing Message objects.

  Returns:
      A new structure with Message objects converted to dictionaries.
  """
  if isinstance(data, messages.Message):
    dict_data = encoding.MessageToDict(data)
    return _convert_message_to_dict(dict_data)
  elif isinstance(data, dict):
    new_dict = {}
    for k, v in data.items():
      new_dict[k] = _convert_message_to_dict(v)
    return new_dict
  elif isinstance(data, list):
    return [_convert_message_to_dict(elem) for elem in data]
  else:
    return data


def _check_for_incompatibility(
    input_data: Mapping[str, any], input_type: feature_helper.InputType
) -> Sequence[any]:
  """Check for incompatibility features in the input yaml."""
  incompatible_list: List[any] = []
  feature_config = feature_helper.get_feature_config()
  unsupported_features = feature_helper.get_feature_list_by_input_type(
      input_type, feature_config.unsupported
  )
  range_limited_features = feature_helper.get_feature_list_by_input_type(
      input_type, feature_config.range_limited
  )
  value_restricted_features = feature_helper.get_feature_list_by_input_type(
      input_type, feature_config.value_limited
  )
  processed_input_data = input_data
  if input_type == feature_helper.InputType.ADMIN_API:
    processed_input_data = _convert_message_to_dict(input_data)
  input_key_value_pairs = util.flatten_keys(processed_input_data, '')
  for key, val in input_key_value_pairs.items():
    # Check for unsupported features.
    if key in unsupported_features:
      if (
          key.startswith('inboundServices')
          or key.startswith('inbound_services')
      ) and get_length(val) > 0:
        incompatible_list.append(unsupported_features[key])
        continue
      if (
          key.startswith('errorHandlers') or key.startswith('error_handlers')
      ) and get_length(val) > 0:
        incompatible_list.append(unsupported_features[key])
        continue
      if key == 'handlers' and not check_for_urlmap_conditions(val, input_type):
        incompatible_list.append(unsupported_features[key])
        continue
      if key in ['app_engine_apis', 'appEngineApis'] and val:
        incompatible_list.append(unsupported_features[key])
        continue
      if (
          key in ['resources.disk_size_gb', 'resources.diskGb']
          and val is not None
      ):
        try:
          if float(val) > 0:
            incompatible_list.append(unsupported_features[key])
        except (ValueError, TypeError):
          pass
        continue
    # Check for basic scaling features.
    if key in [
        'basic_scaling.max_instances',
        'basicScaling.maxInstances',
        'basic_scaling.min_instances',
        'basicScaling.minInstances',
    ]:
      incompatible_list.append(range_limited_features[key])
      continue
    # Check for range_limited features.
    if key in range_limited_features:
      if not range_limited_features[key].validate(val):
        incompatible_list.append(range_limited_features[key])
    # Check for value_restricted features.
    if key in value_restricted_features:
      # TODO: b/493877807 - Better handle runtime validation for flex.
      if util.is_flex_env(input_data) and key.startswith('runtime'):
        continue
      if not value_restricted_features[key].validate(key, val):
        incompatible_list.append(value_restricted_features[key])
  return incompatible_list


def _generate_output(
    incompatible_features: List[feature_helper.UnsupportedFeature],
    input_type: feature_helper.InputType,
    input_name: str,
) -> None:
  """Generate readable output for features compatibility check result."""
  print(f'List incompatible features output for {input_name}:\n')
  logging.info('list-incompatible-features output for %s:\n', input_name)
  if not incompatible_features:
    print('No incompatibilities found.\n')
    logging.info('No incompatibilities found.\n')
    return
  major_features = []
  minor_features = []
  for feature in incompatible_features:
    if feature.severity == 'major':
      major_features.append(feature)
    elif feature.severity == 'minor':
      minor_features.append(feature)
  if minor_features:
    print(
        f'Summary:\nminor: {len(minor_features)}\n'
        f'incompatible_features\n{yaml.dump(_get_display_features(minor_features, input_type))}\n'
    )
    logging.info(
        'Summary:\nminor: %s\nincompatible_features\n%s',
        len(minor_features),
        yaml.dump(_get_display_features(minor_features, input_type)),
    )
  if major_features:
    display_major_features = yaml.dump(
        _get_display_features(major_features, input_type)
    )
    error_message = (
        f'Summary:\nmajor: {len(major_features)}\n'
        f'incompatible_features\n{display_major_features}\n '
    )
    raise IncompatibleFeaturesFoundError(
        error_message
    )


def _get_display_features(
    features: List[feature_helper.UnsupportedFeature],
    input_type: feature_helper.InputType
) -> Sequence[Dict[str, str]]:
  """Converts UnsupportedFeature objects to a display format.

  Args:
      features: A list of UnsupportedFeature objects.
      input_type: The input type of the app.yaml file.

  Returns:
      A list of dictionaries suitable for printing.
  """
  features_display = []
  for feature in features:
    features_display.append({
        'message': feature.reason,
        'category': feature.path[input_type.value],
        'severity': feature.severity,
    })
  return features_display
