###########################################################################
#
#  Copyright 2020 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
#
#      https://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.
#
###########################################################################
"""Handles creation and updates of Placements."""

from starthinker.task.traffic.dao import BaseDAO
from starthinker.task.traffic.campaign import CampaignDAO
from starthinker.task.traffic.video_format import VideoFormatDAO
from starthinker.task.traffic.feed import FieldMap
from starthinker.task.traffic.placement_group import PlacementGroupDAO
from starthinker.task.traffic.class_extensions import StringExtensions


class PlacementDAO(BaseDAO):
  """Placement data access object.

  Inherits from BaseDAO and implements placement specific logic for creating and
  updating placement.
  """

  cache = {}

  def __init__(self, auth, profile_id, is_admin):
    """Initializes PlacementDAO with profile id and authentication scheme."""
    super(PlacementDAO, self).__init__(auth, profile_id, is_admin)

    self._entity = 'PLACEMENT'

    self.campaign_dao = CampaignDAO(auth, profile_id, is_admin)
    self.video_format_dao = VideoFormatDAO(auth, profile_id, is_admin)
    self.placement_group_dao = PlacementGroupDAO(auth, profile_id, is_admin)

    self._id_field = FieldMap.PLACEMENT_ID
    self._search_field = FieldMap.PLACEMENT_NAME

    self._parent_filter_name = 'campaignIds'
    self._parent_filter_field_name = FieldMap.CAMPAIGN_ID
    self._parent_dao = self.campaign_dao

    self._list_name = 'placements'

    self.cache = PlacementDAO.cache

  def _api(self, iterate=False):
    """Returns an DCM API instance for this DAO."""
    return super(PlacementDAO, self)._api(iterate).placements()

  def _api_sizes(self, iterate=False):
    """Returns an DCM API instance for this DAO."""
    return super(PlacementDAO, self)._api(iterate).sizes()

  def _process_skipability(self, feed_item, item):
    """Process skipability settings.

    Args:
      feed_item: A feed item representing a placement from the bulkdozer feed;
      item: A campaign manager placement object to be updated with the
        skipability settings defined in the feed item
    """
    if feed_item.get(FieldMap.PLACEMENT_SKIPPABLE, False):
      if not 'videoSettings' in item:
        item['videoSettings'] = {}

      item['videoSettings']['skippableSettings'] = {
          'skippable': feed_item.get(FieldMap.PLACEMENT_SKIPPABLE, False),
          'skipOffset': {},
          'progressOffset': {}
      }

      skippable_settings = item['videoSettings']['skippableSettings']

      if feed_item.get(FieldMap.PLACEMENT_SKIP_OFFSET_SECONDS, None):
        skippable_settings['skipOffset']['offsetSeconds'] = feed_item.get(
            FieldMap.PLACEMENT_SKIP_OFFSET_SECONDS, None)

      if feed_item.get(FieldMap.PLACEMENT_SKIP_OFFSET_PERCENTAGE, None):
        skippable_settings['skipOffset']['offsetPercentage'] = feed_item.get(
            FieldMap.PLACEMENT_SKIP_OFFSET_PERCENTAGE, None)

      if feed_item.get(FieldMap.PLACEMENT_PROGRESS_OFFSET_SECONDS, None):
        skippable_settings['progressOffset']['offsetSeconds'] = feed_item.get(
            FieldMap.PLACEMENT_SKIP_OFFSET_SECONDS, None)

      if feed_item.get(FieldMap.PLACEMENT_PROGRESS_OFFSET_PERCENTAGE, None):
        skippable_settings['progressOffset'][
            'offsetPercentage'] = feed_item.get(
                FieldMap.PLACEMENT_SKIP_OFFSET_PERCENTAGE, None)
    else:
      if 'skippableSettings' in item and 'videoSettings' in item:
        del item['videoSettings']['skippableSettings']

  def _process_active_view_and_verification(self, placement, feed_item):
    """Updates / creates active view and verification settings.

    This method updates the CM item by setting or creating active view and
    verification settings based on the Bulkdozer feed configurations.

    Args:
      placement: The CM placement object to be updated.
      feed_item: The Bulkdozer feed item with the configurations.

    Raises:
      Exception: In case the values for active view and verification enumeration
      is invalid.
    """

    if FieldMap.PLACEMENT_ACTIVE_VIEW_AND_VERIFICATION in feed_item:
      if feed_item.get(FieldMap.PLACEMENT_ACTIVE_VIEW_AND_VERIFICATION,
                       None) == 'ON':
        placement['vpaidAdapterChoice'] = 'HTML5'
        placement['videoActiveViewOptOut'] = False
      elif feed_item.get(FieldMap.PLACEMENT_ACTIVE_VIEW_AND_VERIFICATION,
                         None) == 'OFF':
        placement['vpaidAdapterChoice'] = 'DEFAULT'
        placement['videoActiveViewOptOut'] = True
      elif feed_item[
          FieldMap.
          PLACEMENT_ACTIVE_VIEW_AND_VERIFICATION] == 'LET_DCM_DECIDE' or feed_item[
              FieldMap.PLACEMENT_ACTIVE_VIEW_AND_VERIFICATION] == '':
        placement['vpaidAdapterChoice'] = 'DEFAULT'
        placement['videoActiveViewOptOut'] = False
      else:
        raise Exception(
            '%s is not a valid value for the placement Active View and Verification field'
            % feed_item.get(FieldMap.PLACEMENT_ACTIVE_VIEW_AND_VERIFICATION,
                            None))

  def _process_pricing_schedule(self, item, feed_item):
    """Updates / creates pricing schedule settings.

    This method updates the CM item with pricing schedule based on
    configurations from the Bulkdozer feed.

    Args:
      item: the CM placement object to update.
      feed_item: The Bulkdozer feed item representing the settings to define.
    """
    if 'pricing_schedule' in feed_item and feed_item['pricing_schedule']:
      if not 'pricingSchedule' in item:
        item['pricingSchedule'] = {}

      item['pricingSchedule']['pricingPeriods'] = []

      for pricing_schedule in feed_item['pricing_schedule']:
        item['pricingSchedule']['pricingPeriods'].append({
            'endDate':
                pricing_schedule.get(FieldMap.PLACEMENT_PERIOD_END, None),
            'startDate':
                pricing_schedule.get(FieldMap.PLACEMENT_PERIOD_START, None),
            'rateOrCostNanos':
                int(
                    float(pricing_schedule.get(FieldMap.PLACEMENT_PERIOD_RATE))
                    * 1000000000),
            'units':
                pricing_schedule.get(FieldMap.PLACEMENT_PERIOD_UNITS),
        })

  def _process_update(self, item, feed_item):
    """Updates an placement based on the values from the feed.

    Args:
      item: Object representing the placement to be updated, this object is
        updated directly.
      feed_item: Feed item representing placement values from the Bulkdozer
        feed.
    """

    if feed_item.get(FieldMap.CAMPAIGN_ID, '') == '':
      feed_item[FieldMap.CAMPAIGN_ID] = item['campaignId']

    campaign = self.campaign_dao.get(feed_item, required=True)
    placement_group = self.placement_group_dao.get(feed_item, required=True)

    feed_item[FieldMap.CAMPAIGN_ID] = campaign['id']
    feed_item[FieldMap.CAMPAIGN_NAME] = campaign['name']

    if placement_group:
      feed_item[FieldMap.PLACEMENT_GROUP_ID] = placement_group['id']
      feed_item[FieldMap.PLACEMENT_GROUP_NAME] = placement_group['name']
      item['placementGroupId'] = placement_group['id']
    else:
      item['placementGroupId'] = None

    self._process_skipability(feed_item, item)

    item['pricingSchedule']['startDate'] = (
        StringExtensions.convertDateTimeStrToDateStr(
            feed_item.get(FieldMap.PLACEMENT_START_DATE, None))
        if feed_item.get(FieldMap.PLACEMENT_START_DATE, '') else
        item['pricingSchedule']['startDate'])

    item['pricingSchedule']['endDate'] = (
        StringExtensions.convertDateTimeStrToDateStr(
            feed_item.get(FieldMap.PLACEMENT_END_DATE, None)) if feed_item.get(
                FieldMap.PLACEMENT_END_DATE, '') else
        item['pricingSchedule']['endDate'])

    item['pricingSchedule']['pricingType'] = feed_item.get(
        FieldMap.PLACEMENT_PRICING_SCHEDULE_COST_STRUCTURE,
        None) if feed_item.get(
            FieldMap.PLACEMENT_PRICING_SCHEDULE_COST_STRUCTURE,
            '') else item['pricingSchedule']['pricingType']

    if feed_item.get(FieldMap.PLACEMENT_PRICING_TESTING_START, None):
      item['pricingSchedule']['testingStartDate'] = feed_item.get(
          FieldMap.PLACEMENT_PRICING_TESTING_START, None)

    item['name'] = feed_item.get(FieldMap.PLACEMENT_NAME,
                                 None) if feed_item.get(FieldMap.PLACEMENT_NAME,
                                                        '') else item['name']
    item['archived'] = feed_item.get(
        FieldMap.PLACEMENT_ARCHIVED, None) if feed_item.get(
            FieldMap.PLACEMENT_ARCHIVED, '') else item['archived']
    item['adBlockingOptOut'] = feed_item.get(FieldMap.PLACEMENT_AD_BLOCKING,
                                             False)

    self._process_transcode(item, feed_item)
    self._process_active_view_and_verification(item, feed_item)
    self._process_pricing_schedule(item, feed_item)

    if feed_item.get(FieldMap.PLACEMENT_ADDITIONAL_KEY_VALUES, None):
      if not 'tagSetting' in item:
        item['tagSetting'] = {}

      item['tagSetting']['additionalKeyValues'] = feed_item.get(
          FieldMap.PLACEMENT_ADDITIONAL_KEY_VALUES, None)

  def _process_transcode(self, item, feed_item):
    """Updates / creates transcode configuration for the placement.

    This method updates the CM placement object with transcoding configuration
    from the feed.

    Args:
      item: The CM placement object to update.
      feed_item: The Bulkdozer feed item with the transcode configurations.
    """
    if feed_item.get('transcode_config', None):
      if not 'videoSettings' in item:
        item['videoSettings'] = {}

      if not 'transcodeSettings' in item['videoSettings']:
        item['videoSettings']['transcodeSettings'] = {}

      item['videoSettings']['transcodeSettings'][
          'enabledVideoFormats'] = self.video_format_dao.translate_transcode_config(
              feed_item['transcode_config'])

      if not item['videoSettings']['transcodeSettings']['enabledVideoFormats']:
        raise Exception(
            'Specified transcode profile did not match any placement level transcode settings in Campaign Manager'
        )

  def get_sizes(self, width, height):
    """Retrieves a creative sizes from DCM.

    Args:
      width: width of the creative.
      height: height of the creative.
      retry_count: how many times the api call should be retried in case of an
        api related error that is not a client error.

    Returns:
      The sizes object from DCM.
    """
    # TODO (mauriciod): this could potentially be in a separate SizesDAO,
    # but since we don't use it anywhere else it is probably fine.
    # May need to do it in case it becomes necessary for other entities when
    # we implement display
    return list(
        self._api_sizes(iterate=True).list(
            profileId=self.profile_id, height=height, width=width).execute())

  def _process_new(self, feed_item):
    """Creates a new placement DCM object from a feed item representing an placement from the Bulkdozer feed.

    This function simply creates the object to be inserted later by the BaseDAO
    object.

    Args:
      feed_item: Feed item representing the placement from the Bulkdozer feed.

    Returns:
      An placement object ready to be inserted in DCM through the API.

    """
    campaign = self.campaign_dao.get(feed_item, required=True)
    placement_group = self.placement_group_dao.get(feed_item, required=True)

    feed_item[FieldMap.CAMPAIGN_ID] = campaign['id']
    feed_item[FieldMap.CAMPAIGN_NAME] = campaign['name']

    if placement_group:
      feed_item[FieldMap.PLACEMENT_GROUP_ID] = placement_group['id']
      feed_item[FieldMap.PLACEMENT_GROUP_NAME] = placement_group['name']

    result = {
        'name':
            feed_item.get(FieldMap.PLACEMENT_NAME, None),
        'adBlockingOptOut':
            feed_item.get(FieldMap.PLACEMENT_AD_BLOCKING, False),
        'campaignId':
            campaign['id'],
        'placementGroupId':
            placement_group['id'] if placement_group else None,
        'archived':
            feed_item.get(FieldMap.PLACEMENT_ARCHIVED, False),
        'siteId':
            feed_item.get(FieldMap.SITE_ID, None),
        'paymentSource':
            'PLACEMENT_AGENCY_PAID',
        'pricingSchedule': {
            'startDate':
                StringExtensions.convertDateTimeStrToDateStr(
                    feed_item.get(FieldMap.PLACEMENT_START_DATE, None)),
            'endDate':
                StringExtensions.convertDateTimeStrToDateStr(
                    feed_item.get(FieldMap.PLACEMENT_END_DATE, None)),
            'pricingType':
                feed_item.get(
                    FieldMap.PLACEMENT_PRICING_SCHEDULE_COST_STRUCTURE, None)
                or 'PRICING_TYPE_CPM',
            'pricingPeriods': [{
                'startDate': feed_item.get(FieldMap.PLACEMENT_START_DATE, None),
                'endDate': feed_item.get(FieldMap.PLACEMENT_END_DATE, None)
            }]
        }
    }

    self._process_skipability(feed_item, result)

    if feed_item.get(FieldMap.PLACEMENT_ADDITIONAL_KEY_VALUES, None):
      result['tagSetting'] = {
          'additionalKeyValues':
              feed_item.get(FieldMap.PLACEMENT_ADDITIONAL_KEY_VALUES, None)
      }

    if feed_item.get(FieldMap.PLACEMENT_PRICING_TESTING_START, None):
      result['pricingSchedule']['testingStartDate'] = feed_item.get(
          FieldMap.PLACEMENT_PRICING_TESTING_START, None)

    self._process_active_view_and_verification(result, feed_item)

    if feed_item.get(FieldMap.PLACEMENT_TYPE, None) == 'VIDEO' or feed_item[
        FieldMap.PLACEMENT_TYPE] == 'IN_STREAM_VIDEO':
      result['compatibility'] = 'IN_STREAM_VIDEO'
      result['size'] = {'width': '0', 'height': '0'}
      result['tagFormats'] = ['PLACEMENT_TAG_INSTREAM_VIDEO_PREFETCH']
    elif feed_item[FieldMap.PLACEMENT_TYPE] == 'IN_STREAM_AUDIO':
       result['compatibility'] = 'IN_STREAM_AUDIO'
       result['size'] = {'width': '0', 'height': '0'}
       result['tagFormats'] = ['PLACEMENT_TAG_INSTREAM_VIDEO_PREFETCH']
    else:
      result['compatibility'] = 'DISPLAY'
      width = 1
      height = 1
      raw_size = feed_item.get(FieldMap.ASSET_SIZE, '0x0')
      if (raw_size and 'x' in raw_size):
        width, height = raw_size.strip().lower().split('x')

      sizes = self.get_sizes(int(width), int(height))

      if sizes:
        result['size'] = {'id': sizes[0]['id']}
      else:
        result['size'] = {'width': int(width), 'height': int(height)}

      result['tagFormats'] = [
          'PLACEMENT_TAG_STANDARD', 'PLACEMENT_TAG_JAVASCRIPT',
          'PLACEMENT_TAG_IFRAME_JAVASCRIPT', 'PLACEMENT_TAG_IFRAME_ILAYER',
          'PLACEMENT_TAG_INTERNAL_REDIRECT', 'PLACEMENT_TAG_TRACKING',
          'PLACEMENT_TAG_TRACKING_IFRAME', 'PLACEMENT_TAG_TRACKING_JAVASCRIPT'
      ]

    self._process_transcode(result, feed_item)
    self._process_pricing_schedule(result, feed_item)

    return result

  def _post_process(self, feed_item, item):

    for pricing_schedule in feed_item.get('pricing_schedule', []):
      placement = self.get(pricing_schedule)

      if placement:
        feed_item[FieldMap.PLACEMENT_ID] = placement['id']

  def map_placement_transcode_configs(self, placement_feed,
                                      transcode_configs_feed,
                                      pricing_schedule_feed):
    """Maps sub feeds with the parent feed based on placement id.

    Args:
      placement_feed: Bulkdozer feed representing the placements configurations.
      trascode_configs_feed: Bulkdozer feed representing the transcode configs.
      pricing_schedule_feed: Bulkdozer feed representing the pricing schedules.
    """

    for placement in placement_feed:
      placement['pricing_schedule'] = []

      for pricing_schedule in pricing_schedule_feed:
        if placement.get(FieldMap.PLACEMENT_ID,
                         '') == pricing_schedule.get(FieldMap.PLACEMENT_ID,
                                                     None):
          placement['pricing_schedule'].append(pricing_schedule)

      transcode_id = placement.get(FieldMap.TRANSCODE_ID, '')
      placement['transcode_config'] = []
      if transcode_id:
        for transcode_config in transcode_configs_feed:
          if transcode_id == transcode_config.get(FieldMap.TRANSCODE_ID, None):
            placement['transcode_config'].append(transcode_config)
