"""Facebook API"""
import os
import json
import logging
import pandas as pd
import time
import numpy as np
from datetime import datetime
from facebook_business.api import FacebookAdsApi
from facebook_business.adobjects.adaccount import AdAccount
from facebook_business.adobjects.serverside.event import Event
from facebook_business.adobjects.serverside.event_request import EventRequest
from facebook_business.adobjects.serverside.user_data import UserData
from facebook_business.adobjects.serverside.custom_data import CustomData
from facebook_business.exceptions import FacebookRequestError


def transform_campaign_budget(campaigns):
    """
    Transforms get_campaigns response.
    """
    out = []
    for campaign in campaigns:
        campaign_dict = dict(campaign)
        if "lifetime_budget" in campaign_dict:
            campaign_dict["budget"] = campaign_dict["lifetime_budget"]
            campaign_dict["budget_type"] = "lifetime_budget"
            del campaign_dict["lifetime_budget"]
        elif "daily_budget" in campaign_dict:
            campaign_dict["budget"] = campaign_dict["daily_budget"]
            campaign_dict['budget_type'] = "daily_budget"
            del campaign_dict["daily_budget"]
        out.append(campaign_dict)

    data = pd.DataFrame(out).rename(
        columns={
            "id": "campaign_id",
            "name": "campaign_name"
        }
    )
    return data


def build_predicted_revenue_events(df, event_name):
    """
    Creates a list of Facebook Event objects which can be pushed to the Facebook Conversions API.
    Also creates DataFrame for logging which can be used to stream insert to a BigQuery log table.

    :param df: A DataFrame with the events to build Facebook events for
    :type df: pd.DataFrame

    Returns: A tuple with a list of Facebook events and a DataFrame for logs
    rtype: (list of Event, pd.DataFrame)
    """
    events = []
    logs = []
    for index, row in df.iterrows():
        date = int(time.mktime(datetime.strptime(row['date'], '%Y%m%d').timetuple()))
        user_data = UserData(
            country_code=row['shop'],
            fbp=row['facebook_browser_id']
        )

        custom_data = CustomData(
            currency=row['currency'],
            value=row['predicted_revenue']
        )

        event = Event(
            event_name=event_name,
            event_time=date,
            user_data=user_data,
            custom_data=custom_data,
            data_processing_options=[]
        )
        events.append(event)

        logs.append(
            {
                "facebook_browser_id": row['facebook_browser_id'],
                "shop": row['shop'],
                "date_source": row['date'],
                "date_processed": datetime.today().strftime('%Y-%m-%d-%H:%M:%S'),
                "predicted_revenue": row['predicted_revenue'],
                "currency": row['currency']
            }
        )
    df_logs = pd.DataFrame(logs)
    return events, df_logs


def calculate_batches(total_events, batch_size):
    """
    Calculate number of batches to split events into given a batch size and taking into account evenly divisible batches
    :param total_events: The number of total events
    :type total_events: int
    :param batch_size: The max number of events in a batch
    :type batch_size: int

    Returns: The resulting number of batches
    rtype: int

    """
    batches = None
    try:
        batches = (total_events // batch_size)
        batches = batches + 1 if total_events % batch_size != 0 else batches
    except ZeroDivisionError:
        logging.error('Batch Size cannot be 0')
        raise
    except TypeError:
        logging.error('Total Events and Batch Size must be integers')
        raise
    return batches


def split_events_to_batches(events, batch_size=1000):
    """
    Divides a DataFrame of events into batches of the specified batch size (defaults to 1000).

    :param events: A DataFrame of Facebook Events to push to the conversions API
    :type events: pd.DataFrame
    :param batch_size: The max number of events in a batch
    :type batch_size: int

    Returns: A list of DataFrames
    rtype: list of pd.DataFrame

    """
    batched_df = []
    try:
        total_events = len(events.index)
    except TypeError:
        logging.warning("Events is null")

    if total_events > 0:
        logging.info('Batch limit set to %s', batch_size)
        if total_events > batch_size:
            batches = calculate_batches(total_events, batch_size)
            batched_df = np.array_split(events, batches)
            logging.info('Total %s events split into %s batches', total_events, batches)
        else:
            batched_df = [events]
            logging.info('Total %s events only requires 1 batch', total_events)
    else:
        logging.warning('No events split into batches')
    return batched_df


class FacebookExecutor:
    """ Facebook FacebookExecutor.
    Arguments:
        """
    def __init__(self):
        self.client = None
        self.access_token = None
        self.account = None
        self.account_id = None
        self.pixel_id = None
        self.set_api_config()

    def set_api_config(self):
        """
        Loads access_token from FACEBOOK_APPLICATION_CREDENTIALS.
        """
        try:
            with open(os.environ["FACEBOOK_APPLICATION_CREDENTIALS"]) as facebook_cred:
                data = json.load(facebook_cred)
                self.access_token = data["access_token"]
        except KeyError:
            raise KeyError("FACEBOOK_APPLICATION_CREDENTIALS env variable needed")
        self.set_client()

    def set_client(self):
        """
        Sets self.client using the access token.
        """
        self.client = FacebookAdsApi.init(access_token=self.access_token)

    def set_account(self, account_id):
        """ Sets account object
        """
        self.account_id = account_id
        self.account = AdAccount('act_{}'.format(self.account_id))
        logging.info("Initiated AdAccount object for account %s", self.account_id)

    def set_pixel_id(self, pixel_id):
        """ Sets the Pixel ID
        """
        self.pixel_id = pixel_id
        logging.info("Set the pixel_id as %s", self.pixel_id)

    def get_campaign_insights(self, account_id, fields, start_date, end_date):
        """
        Sets insights from the Facebook Insight API.
        Parameters:
            account_id: ID associated to the Facebook Account
            start_date: The start date
            end_date: The end date
            fields: list of field to be fetched
            start_date/end_date: defines the time range to get insights for (YYYY-mm-dd).
        """
        self.set_account(account_id)
        out = []
        params = {
            'effective_status': ['ACTIVE'],
            'level': 'campaign',
            'time_range': {
                'since': start_date,
                'until': end_date
                }
        }
        logging.debug("Downloading insights for account %s", self.account_id)
        logging.debug("fields: %s", fields)
        logging.debug("params: %s", params)
        campaign_insights = self.account.get_insights(
            params=params,
            fields=fields
        )

        for insight in campaign_insights:
            out.append(dict(insight))
        return out

    def get_active_campaigns(self):
        return self.account.get_campaigns(
            fields=['account_id', 'name', 'daily_budget', 'lifetime_budget'],
            params={
                'effective_status': ["ACTIVE"],
                'is_completed': False
            }
        )

    def get_active_campaign_budgets(self, account_id):
        """
        Fetches active campaign metadata from the Facebook API.
        Returns a dataframe with the following fields:
            - account_id
            - campaign_id
            - campaign_name
            - budget_type (daily_budget or lifetime_budget)
            - budget amount in account currency
        """
        self.set_account(account_id)
        campaigns = self.get_active_campaigns()
        out = transform_campaign_budget(campaigns)
        return out

    def update_daily_budget(self, account_id, campaign_id, new_budget):
        """
        Update the budget on the facebook API
        """
        self.set_account(account_id)
        campaigns = self.get_active_campaigns()
        for campaign in campaigns:
            if campaign.get_id() == campaign_id:
                from pygyver.etl.toolkit import configure_logging
                configure_logging()
                logging.info(
                    "Loading new budget for campaign %s",
                    campaign_id
                )
                logging.info(
                    "Current daily_budget for campaign %s: %s",
                    campaign_id,
                    campaign['daily_budget']
                )
                campaign.api_update(
                    params={'daily_budget': round(new_budget*100)}
                )
                logging.info(
                    "New daily_budget for campaign %s: %s",
                    campaign_id,
                    new_budget
                )

        return campaigns

    def push_conversions_api_events(self, events, test_event_code=None):
        """
        Pushes a list of Events to the Facebook Conversions API.

        :param events: A list of Facebook Events to push to the conversions API
        :type events: list of Event
        :param test_event_code: A test_event_code from Facebook Events Manager to mark these as test events
        :type test_event_code: str

        Returns: A dictionary with the parsed response from the Facebook API
        rtype: dict[str, str]
        """
        if len(events) > 1000:
            logging.error("The maximum number of events that Facebook accepts in a single API call is 1,000. "
                          "Please use the split_events_to_batches() function to split the events into batches")
            raise ValueError

        event_request = EventRequest(
            events=events,
            pixel_id=self.pixel_id,
        )

        # Add the test_event_code if one is given
        if test_event_code:
            event_request.test_event_code = test_event_code

        api_response = {}

        try:
            event_response = event_request.execute()
            logging.info('%s events pushed to Facebook Conversions API', event_response.events_received)
            api_response['status'] = 'API Success'
            api_response['fb_trace_id'] = event_response.fbtrace_id
            api_response['messages'] = '\n'.join(event_response.messages)
            api_response['total_events'] = event_response.events_received

        except FacebookRequestError as e:
            logging.error('There was a Facebook Conversions API error:\n\t%s', e)
            api_response['status'] = 'API Error'
            api_response['fb_trace_id'] = e.body()['error']['fbtrace_id']
            error_message = e.body()['error']['message']
            error_message = ': '.join([error_message, e.body()['error']['error_user_msg']])
            api_response['messages'] = error_message
            api_response['total_events'] = None

        return api_response
