# Copyright 2022 Google LLC
#
# 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.
'Prepares descriptors and timeseries for peering group metrics.'

import itertools
import logging

from . import MetricDescriptor, TimeSeries, register_timeseries

DESCRIPTOR_ATTRS = {
    'forwarding_rules_l4_available':
        'L4 fwd rules limit per peering group',
    'forwarding_rules_l4_used':
        'L4 fwd rules used per peering group',
    'forwarding_rules_l4_used_ratio':
        'L4 fwd rules used ratio per peering group',
    'forwarding_rules_l7_available':
        'L7 fwd rules limit per peering group',
    'forwarding_rules_l7_used':
        'L7 fwd rules used per peering group',
    'forwarding_rules_l7_used_ratio':
        'L7 fwd rules used ratio per peering group',
    'instances_available':
        'Instance limit per peering group',
    'instances_used':
        'Instance used per peering group',
    'instances_used_ratio':
        'Instance used ratio per peering group',
    'routes_dynamic_available':
        'Dynamic route limit per peering group',
    'routes_dynamic_used':
        'Dynamic route used per peering group',
    'routes_dynamic_used_ratio':
        'Dynamic route used ratio per peering group',
    'routes_static_available':
        'Static route limit per peering group',
    'routes_static_used':
        'Static route used per peering group',
    'routes_static_used_ratio':
        'Static route used ratio per peering group',
}
LIMITS = {
    'forwarding_rules_l4': {
        'pg': ('INTERNAL_FORWARDING_RULES_PER_PEERING_GROUP', 500),
        'prj': ('INTERNAL_FORWARDING_RULES_PER_NETWORK', 500)
    },
    'forwarding_rules_l7': {
        'pg': ('INTERNAL_MANAGED_FORWARDING_RULES_PER_PEERING_GROUP', 175),
        'prj': ('INTERNAL_MANAGED_FORWARDING_RULES_PER_NETWORK', 75)
    },
    'instances': {
        'pg': ('INSTANCES_PER_PEERING_GROUP', 15500),
        'prj': ('INSTANCES_PER_NETWORK_GLOBAL', 15000)
    },
    'routes_static': {
        'pg': ('STATIC_ROUTES_PER_PEERING_GROUP', 300),
        'prj': ('ROUTES', 250)
    },
    'routes_dynamic': {
        'pg': ('DYNAMIC_ROUTES_PER_PEERING_GROUP', 300),
        'prj': ('', 100)
    }
}
LOGGER = logging.getLogger('net-dash.timeseries.peerings')


def _count_forwarding_rules_l4(resources, network_ids):
  'Returns count of L4 forwarding rules for specified network ids.'
  return len([
      r for r in resources['forwarding_rules'].values() if
      r['network'] in network_ids and r['load_balancing_scheme'] == 'INTERNAL'
  ])


def _count_forwarding_rules_l7(resources, network_ids):
  'Returns count of L7 forwarding rules for specified network ids.'
  return len([
      r for r in resources['forwarding_rules'].values()
      if r['network'] in network_ids and
      r['load_balancing_scheme'] == 'INTERNAL_MANAGED'
  ])


def _count_instances(resources, network_ids):
  'Returns count of instances for specified network ids.'
  count = 0
  for i in resources['instances'].values():
    if any(n['network'] in network_ids for n in i['networks']):
      count += 1
  return count


def _count_routes_static(resources, network_ids):
  'Returns count of static routes for specified network ids.'
  return len(
      [r for r in resources['routes'].values() if r['network'] in network_ids])


def _count_routes_dynamic(resources, network_ids):
  'Returns count of dynamic routes for specified network ids.'
  return sum([
      sum(v.values())
      for k, v in resources['routes_dynamic'].items()
      if k in network_ids
  ])


def _get_limit_max(quota, network_id, project_id, resource_name):
  'Returns maximum limit value in project / peering group / network limits.'
  pg_name, pg_default = LIMITS[resource_name]['pg']
  prj_name, prj_default = LIMITS[resource_name]['prj']
  network_quota = quota.get(network_id, {})
  project_quota = quota.get(project_id, {}).get('global', {})
  return max([
      network_quota.get(pg_name, 0),
      project_quota.get(prj_name, prj_default),
      project_quota.get(pg_name, pg_default)
  ])


def _get_limit(quota, network, resource_name):
  'Computes and returns peering group limit.'
  # reference https://cloud.google.com/vpc/docs/quota#vpc-peering-ilb-example
  # step 1 - vpc_max = max(vpc limit, pg limit)
  vpc_max = _get_limit_max(quota, network['self_link'], network['project_id'],
                           resource_name)
  # step 2 - peers_max = [max(vpc limit, pg limit) for v in peered vpcs]
  # step 3 - peers_min = min(peers_max)
  peers_min = min([
      _get_limit_max(quota, p['network'], p['project_id'], resource_name)
      for p in network['peerings']
  ])
  # step 4 - max(vpc_max, peers_min)
  return max([vpc_max, peers_min])


def _peering_group_timeseries(resources, network):
  'Computes and returns peering group timeseries for network.'
  if len(network['peerings']) == 0:
    return
  network_ids = [network['self_link']
                ] + [p['network'] for p in network['peerings']]
  for resource_name in LIMITS:
    limit = _get_limit(resources['quota'], network, resource_name)
    func = globals().get(f'_count_{resource_name}')
    if not func or not callable(func):
      LOGGER.critical(f'no handler for {resource_name} or handler not callable')
      continue
    count = func(resources, network_ids)
    labels = {'project': network['project_id'], 'network': network['name']}
    yield TimeSeries(f'peering_group/{resource_name}_used', count, labels)
    yield TimeSeries(f'peering_group/{resource_name}_available', limit, labels)
    yield TimeSeries(f'peering_group/{resource_name}_used_ratio', count / limit,
                     labels)


@register_timeseries
def timeseries(resources):
  'Returns peering group timeseries for all networks.'
  LOGGER.info('timeseries')
  # returns metric descriptors
  for dtype, name in DESCRIPTOR_ATTRS.items():
    yield MetricDescriptor(f'peering_group/{dtype}', name,
                           ('project', 'network'), dtype.endswith('ratio'))
  # chain timeseries for each network and return each one individually
  results = itertools.chain(*(_peering_group_timeseries(resources, n)
                              for n in resources['networks'].values()))
  for result in results:
    yield result
