import re
import requests
from http import HTTPStatus
import json as JSON

from infra.config import BaseUri, Settings
from utils.logger import logger, bcolors
from utils.singleton import SingletonMeta

swagger_api_json_endpoint = '/api-json'
api_info_urls = {
  BaseUri.Iguana: 'Iguana',
  BaseUri.Pyrador: 'Pyrador',
  BaseUri.Zoo: 'Zoo'
}
excluded_endpoints = [
  ('POST', BaseUri.Iguana + '/api/v1/test/controller'),
  ('GET', BaseUri.Iguana + '/api/v1/test/controller'),
  ('DELETE', BaseUri.Iguana + '/api/v1/test/controller'),
  ('GET', BaseUri.Iguana + '/api/v1/health'),
  ('GET', BaseUri.Iguana + '/api/v1/metrics'),
  ('GET', BaseUri.Iguana + '/api/v1/settings'),
  ('POST', BaseUri.Iguana + '/api/v1/settings'),
  ('PUT', BaseUri.Iguana + '/api/v1/settings'),
  ('GET', BaseUri.Iguana + '/api/v1/activity'),
  ('GET', BaseUri.Iguana + '/api/v1/activity/{activity_id}'),
  ('POST', BaseUri.Iguana + '/api/v1/doorlock'),
  ('PUT', BaseUri.Iguana + '/api/v1/profile/set-account-number'),
  ('PUT', BaseUri.Iguana + '/api/v1/profile/address'),
  ('PUT', BaseUri.Iguana + '/api/v1/profile/contact'),
  ('POST', BaseUri.Iguana + '/api/v1/profile/set-firebase-token'),
  ('PUT', BaseUri.Iguana + '/api/v1/profile/balance'),
  ('GET', BaseUri.Iguana + '/api/v1/providable-service/{place_id}'),
  ('POST', BaseUri.Iguana + '/api/v1/light-device/toggle'),
  ('GET', BaseUri.Iguana + '/api/v1/light-device/state/{device_id}'),
  ('PUT', BaseUri.Iguana + '/api/v1/user-place/{place_id}'),
  ('GET', BaseUri.Iguana + '/api/v1/user-place/{place_id}/services'),
  ('PUT', BaseUri.Iguana + '/api/v1/user-place/set/status'),
  ('POST', BaseUri.Iguana + '/api/v1/profile/device/to/service'),
  ('DELETE', BaseUri.Iguana + '/api/v1/profile/device/from/service'),
  ('GET', BaseUri.Iguana + '/api/v1/profile/place/{place_id}/service/devices/{device_category}'),
  ('POST', BaseUri.Iguana + '/api/v1/room'),
  ('GET', BaseUri.Iguana + '/api/v1/room/by/place/{parent_id}'),
  ('PUT', BaseUri.Iguana + '/api/v1/room/{id}'),
  ('DELETE', BaseUri.Iguana + '/api/v1/room/{id}'),
  ('GET', BaseUri.Iguana + '/api/v1/device/list/{type}'),
  ('DELETE', BaseUri.Iguana + '/api/v1/user-place/qrcode'),
  ('POST', BaseUri.Iguana + '/api/v1/billing'),
  ('POST', BaseUri.Iguana + '/api/v1/intercom/acceptCall'), # TODO: test it with notifications
  ('POST', BaseUri.Iguana + '/api/v1/upload/avatar'), # TODO: unable to test
  ('POST', BaseUri.Zoo + '/api/v1/notifications/send-notification'),
  ('POST', BaseUri.Zoo + '/api/v1/notifications/send-sms'),
  ('DELETE', BaseUri.Zoo + '/api/v1/place/available_services')
]

class APICoverageTracker(metaclass=SingletonMeta):
  def __init__(self):
    self.called_endpoints = {}
    self.api_info = self.request_api_info(api_info_urls)
    self.build_called_endpoints()

  def request_api_info(self, urls):
    api_info = {}

    for url in urls:
      res = requests.get(url + swagger_api_json_endpoint)
      api_info[url] = res.json()

    return api_info

  def build_called_endpoints(self):
    for url, info in self.api_info.items():
      try:
        paths = info.get('paths')
        if not url in self.called_endpoints:
          self.called_endpoints[url] = {}
        for path, methods in paths.items():
          endpoint = url + path
          self.called_endpoints[url][path] = {}
          for method, method_info in methods.items():
            if (method.upper(), endpoint) in excluded_endpoints:
              continue
            self.called_endpoints[url][path][method] = 0
      except Exception as e:
        logger.error('Error happened while getting api info:', e)

  def endpoint_is_called(self, called_endpoint, method):
    if not Settings.EnableCoverageStatistics:
      return

    for url, paths in self.called_endpoints.items():
      for path, methods in paths.items():
        endpoint = url + path
        pattern = re.sub(r'{.+?}', r'[^/]+', endpoint) + '$'
        if re.match(pattern, called_endpoint) and method.lower() in methods:
          self.called_endpoints[url][path][method.lower()] += 1
          return

  def print_coverage(self):
    def calculate_coverage_statistics(total_urls, covered_urls):
      if total_urls == 0:
        return 0
      coverage_percentage = int(covered_urls / total_urls * 100)

      if coverage_percentage < 50:
        color = bcolors.FAIL
      elif coverage_percentage < 75:
        color = bcolors.WARNING
      else:
        color = bcolors.OKGREEN

      statistics = f'{coverage_percentage}% ({covered_urls} / {total_urls})'
      return f'{color}{statistics}{bcolors.ENDC}'

    def count_urls(gateway_url):
      urls_num = 0
      covered_urls_num = 0

      for url, paths in self.called_endpoints.items():
        for path, methods in paths.items():
          endpoint = url + path
          if gateway_url in endpoint:
            for method, num_of_calls in methods.items():
              urls_num += 1
              if num_of_calls > 0:
                covered_urls_num += 1
              else:
                logger.warn(f'{method.upper()} {endpoint} is not covered')

      return urls_num, covered_urls_num

    if not Settings.EnableCoverageStatistics:
      return

    urls_num_sum = 0
    covered_urls_num_sum = 0

    urls_info = \
      [(gateway_name, count_urls(gateway_url)) \
        for gateway_url, gateway_name in api_info_urls.items()]

    logger.info('Coverage statistics:')
    logger.info()

    for gateway_name, (urls_num, covered_urls_num) in urls_info:
      coverage_statistics = calculate_coverage_statistics(urls_num, covered_urls_num)
      message = f'    {gateway_name}: {coverage_statistics}'
      logger.info(message)

      urls_num_sum += urls_num
      covered_urls_num_sum += covered_urls_num

    coverage_statistics = \
      calculate_coverage_statistics(urls_num_sum, covered_urls_num_sum)
    logger.info()
    logger.info(f'    Total: {coverage_statistics}\n')

class Response(requests.Response):
  def __init__(self, status_code=HTTPStatus.OK):
    super().__init__()
    self.status_code = status_code

def log_req(method, url, params=None, data=None, json=None, headers=None):
  logger.verbose(f'============================================================')
  logger.verbose(f'[REQUEST] {method} {url}')
  if params:
    logger.verbose(f'params: {params}')
  if data:
    data = JSON.dumps(data, sort_keys=True, indent=4)
    logger.verbose(f'data: {data}')
  if json:
    json = JSON.dumps(json, sort_keys=True, indent=4)
    logger.verbose(f'json: {json}')
  if headers:
    headers = JSON.dumps(headers, sort_keys=True, indent=4)
    logger.verbose(f'headers: {headers}')

def log_res(res: requests.Response):
  req = res.request
  logger.verbose(f'[RESPONSE] {req.method} {req.url} {res.status_code}')
  try:
    json = JSON.dumps(res.json(), sort_keys=True, indent=4).replace('\\"', '"')
    lines_num = json.count('\n')
    max_lines_num = Settings.LoggingResponseMaxLinesNum
    if lines_num <= max_lines_num:
      logger.verbose(f'json: {json}')
    else:
      stats = f'{lines_num}/{max_lines_num}'
      logger.verbose(f'Maximum number of lines for response exceeded:', stats)
  except ValueError:
    logger.verbose('response:', res.content)
  except Exception as e:
    logger.verbose(e)

def request(method, url, headers=None, **kwargs):
  APICoverageTracker().endpoint_is_called(url, method)
  log_req(method, url, params=kwargs.get('params'), \
    data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
  res = requests.request(method, url, **kwargs)
  log_res(res)
  return res

def get(url, params=None, headers=None, **kwargs):
  method = 'GET'
  APICoverageTracker().endpoint_is_called(url, method)
  log_req(method, url, params=params, \
    data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
  res = requests.get(url, params=params, headers=headers, **kwargs)
  log_res(res)
  return res

def options(url, headers=None, **kwargs):
  method = 'OPTIONS'
  APICoverageTracker().endpoint_is_called(url, method)
  log_req(method, url, params=kwargs.get('params'), \
    data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
  res = requests.options(url, headers=headers, **kwargs)
  log_res(res)
  return res

def head(url, headers=None, **kwargs):
  method = 'HEAD'
  APICoverageTracker().endpoint_is_called(url, method)
  log_req(method, url, params=kwargs.get('params'), \
    data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
  res = requests.head(url, headers=headers, **kwargs)
  log_res(res)
  return res

def post(url, data=None, json=None, headers=None, **kwargs):
  method = 'POST'
  APICoverageTracker().endpoint_is_called(url, method)
  log_req(method, url, params=kwargs.get('params'), \
    data=data, json=json, headers=headers)
  res = requests.post(url, data=data, json=json, headers=headers, **kwargs)
  log_res(res)
  return res

def put(url, data=None, headers=None, **kwargs):
  method = 'PUT'
  APICoverageTracker().endpoint_is_called(url, method)
  log_req(method, url, params=kwargs.get('params'), \
    data=data, json=kwargs.get('json'), headers=headers),
  res = requests.put(url, data=data, headers=headers, **kwargs)
  log_res(res)
  return res

def patch(url, data=None, headers=None, **kwargs):
  method = 'PATCH'
  APICoverageTracker().endpoint_is_called(url, method)
  log_req(method, url, params=kwargs.get('params'), \
    data=data, json=kwargs.get('json'), headers=headers)
  res = requests.patch(url, data=data, headers=headers, **kwargs)
  log_res(res)
  return res

def delete(url, headers=None, **kwargs):
  method = 'DELETE'
  APICoverageTracker().endpoint_is_called(url, method)
  log_req(method, url, params=kwargs.get('params'), \
    data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
  res = requests.delete(url, headers=headers, **kwargs)
  log_res(res)
  return res