""" Module which handles the follow features like unfollowing and following """
# import built-in & third-party modules
import os
import random
import json
import csv
import sqlite3

from datetime import datetime
from datetime import timedelta
from math import ceil

# import InstaPy modules
from .time_util import sleep
from .util import delete_line_from_file
from .util import format_number
from .util import update_activity
from .util import add_user_to_blacklist
from .util import click_element
from .util import web_address_navigator
from .util import get_relationship_counts
from .util import emergency_exit
from .util import find_user_id
from .util import is_page_available
from .util import reload_webpage
from .util import click_visibly
from .util import get_action_delay
from .util import truncate_float
from .util import get_query_hash
from .util import is_follow_me
from .util import get_epoch_time_diff
from .follow_util import get_following_status
from .print_log_writer import log_followed_pool
from .print_log_writer import log_uncertain_unfollowed_pool
from .print_log_writer import log_record_all_unfollowed
from .print_log_writer import get_log_time
from .relationship_tools import get_followers
from .relationship_tools import get_nonfollowers
from .relationship_tools import get_following
from .database_engine import get_database
from .quota_supervisor import quota_supervisor
from .event import Event
from .xpath import read_xpath

# import exceptions
from selenium.common.exceptions import WebDriverException
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import ElementNotVisibleException


def set_automated_followed_pool(
    username,
    unfollow_after,
    logger,
    logfolder,
    delay_followbackers,
    pool="followedPool",
):
    """Generate a user list based on the InstaPy followed usernames"""
    pool_name = "{0}{1}_{2}.csv".format(logfolder, username, pool)
    automatedFollowedPool = {"all": {}, "eligible": {}}
    time_stamp = None
    user = None

    try:
        with open(pool_name, "r+") as followedPoolFile:
            reader = csv.reader(followedPoolFile)

            for row in reader:

                # init
                followedback = None
                user_id = "undefined"  # 'undefined' rather than None is *intentional
                eligle = True  # 'undefined' rather than None is *intentional

                entries = row[0].split(" ~ ")
                sz = len(entries)

                # Data entry styles [historically]:
                #    user,   # oldest
                #    datetime ~ user,   # after `unfollow_after` was introduced
                #    datetime ~ user ~ user_id,   # after `user_id` was added
                if sz == 1:
                    time_stamp = None
                    user = entries[0]

                elif sz == 2:
                    time_stamp = entries[0]
                    user = entries[1]

                elif sz == 3:
                    time_stamp = entries[0]
                    user = entries[1]
                    user_id = entries[2]

                elif sz == 4:
                    time_stamp = entries[0]
                    user = entries[1]
                    user_id = entries[2]
                    followedback = True if entries[3] == "true" else None

                automatedFollowedPool["all"].update(
                    {
                        user: {
                            "id": user_id,
                            "time_stamp": time_stamp,
                            "followedback": followedback,
                        }
                    }
                )
                # get eligible list
                if time_stamp is not None:
                    # filter the eligible users by
                    # unfollow_after seconds
                    # or by delay_followbackers seconds
                    delay_unfollow = True
                    unfollow_after_eligible = None

                    if followedback is True and delay_followbackers:
                        # delay length is for follow backers
                        unfollow_after_eligible = delay_followbackers
                    elif unfollow_after is not None:
                        # delay length is set by admin
                        unfollow_after_eligible = unfollow_after
                    else:
                        delay_unfollow = False

                    if delay_unfollow:
                        time_diff = get_epoch_time_diff(time_stamp, logger)
                        if time_diff is None:
                            continue

                        if time_diff < unfollow_after_eligible:
                            eligle = False

                if eligle:
                    automatedFollowedPool["eligible"].update({user: {"id": user_id}})

        followedPoolFile.close()

    except BaseException as exc:
        logger.error(
            "Error occurred while generating a user list from the followed "
            "pool!\n\t{}".format(str(exc).encode("utf-8"))
        )

    return automatedFollowedPool


def unfollow(
    browser,
    username,
    amount,
    customList,
    InstapyFollowed,
    nonFollowers,
    allFollowing,
    style,
    automatedFollowedPool,
    relationship_data,
    dont_include,
    white_list,
    sleep_delay,
    jumps,
    delay_followbackers,
    logger,
    logfolder,
):
    """Unfollows the given amount of users"""

    msg = None
    unfollow_num = 0
    unfollow_list = None
    unfollow_track = None
    unfollow_state = None
    customList_data = None
    user_link = "https://www.instagram.com/{}/".format(username)

    if (
        customList is not None
        and isinstance(customList, (tuple, list))
        and len(customList) == 3
        and customList[0] is True
        and isinstance(customList[1], (list, tuple, set))
        and len(customList[1]) > 0
        and customList[2] in ["all", "nonfollowers"]
    ):
        customList_data = customList[1]
        if not isinstance(customList_data, list):
            customList_data = list(customList_data)
        unfollow_track = customList[2]
        customList = True
    else:
        customList = False

    if (
        InstapyFollowed is not None
        and isinstance(InstapyFollowed, (tuple, list))
        and len(InstapyFollowed) == 2
        and InstapyFollowed[0] is True
        and InstapyFollowed[1] in ["all", "nonfollowers"]
    ):
        unfollow_track = InstapyFollowed[1]
        InstapyFollowed = True
    else:
        InstapyFollowed = False

    # check URL of the webpage, if it already is the one to be navigated
    # then do not navigate to it again
    web_address_navigator(browser, user_link)

    # check how many poeple we are following
    _, allfollowing = get_relationship_counts(browser, username, logger)

    if allfollowing is None:
        logger.warning(
            "Unable to find the count of users followed  ~leaving unfollow feature"
        )
        return 0
    elif allfollowing == 0:
        logger.warning("There are 0 people to unfollow  ~leaving unfollow feature")
        return 0

    if amount > allfollowing:
        logger.info(
            "There are less users to unfollow than you have requested:  "
            "{}/{}  ~using available amount\n".format(allfollowing, amount)
        )
        amount = allfollowing

    if (
        customList is True
        or InstapyFollowed is True
        or nonFollowers is True
        or allFollowing is True
    ):

        if nonFollowers is True:
            InstapyFollowed = False

        if customList is True:
            logger.info("Unfollowing from the list of pre-defined usernames\n")
            unfollow_list = customList_data

        elif InstapyFollowed is True:
            logger.info("Unfollowing the users followed by InstaPy\n")
            unfollow_list = list(automatedFollowedPool["eligible"].keys())

        elif nonFollowers is True:
            logger.info("Unfollowing the users who do not follow back\n")

            # Unfollow only the users who do not follow you back
            unfollow_list = get_nonfollowers(
                browser,
                username,
                username,
                relationship_data,
                False,
                True,
                logger,
                logfolder,
            )

        # pick only the users in the right track- ["all" or "nonfollowers"]
        # for `customList` and
        #  `InstapyFollowed` unfollow methods
        if customList is True or InstapyFollowed is True:
            if unfollow_track == "nonfollowers":
                all_followers = get_followers(
                    browser,
                    username,
                    username,
                    "full",
                    relationship_data,
                    False,
                    True,
                    logger,
                    logfolder,
                )
                loyal_users = [user for user in unfollow_list if user in all_followers]
                logger.info(
                    "Found {} loyal followers!  ~will not unfollow "
                    "them".format(len(loyal_users))
                )
                unfollow_list = [
                    user for user in unfollow_list if user not in loyal_users
                ]

            elif unfollow_track != "all":
                logger.info(
                    'Unfollow track is not specified! ~choose "all" or '
                    '"nonfollowers"'
                )
                return 0

        # re-generate unfollow list according to the `unfollow_after`
        # parameter for `customList` and
        #  `nonFollowers` unfollow methods
        if customList is True or nonFollowers is True:
            not_found = []
            non_eligible = []
            for person in unfollow_list:
                if person not in automatedFollowedPool["all"].keys():
                    not_found.append(person)
                elif (
                    person in automatedFollowedPool["all"].keys()
                    and person not in automatedFollowedPool["eligible"].keys()
                ):
                    non_eligible.append(person)

            unfollow_list = [user for user in unfollow_list if user not in non_eligible]
            logger.info(
                "Total {} users available to unfollow"
                "  ~not found in 'followedPool.csv': {}  |  didn't "
                "pass `unfollow_after`: {}\n".format(
                    len(unfollow_list), len(not_found), len(non_eligible)
                )
            )

        elif InstapyFollowed is True:
            non_eligible = [
                user
                for user in automatedFollowedPool["all"].keys()
                if user not in automatedFollowedPool["eligible"].keys()
            ]
            logger.info(
                "Total {} users available to unfollow  ~didn't pass "
                "`unfollow_after`: {}\n".format(len(unfollow_list), len(non_eligible))
            )
        elif allFollowing is True:
            logger.info("Unfollowing the users you are following")
            unfollow_list = get_following(
                browser,
                username,
                username,
                "full",
                relationship_data,
                False,
                True,
                logger,
                logfolder,
            )

        if len(unfollow_list) < 1:
            logger.info("There are no any users available to unfollow")
            return 0

        # choose the desired order of the elements
        if style == "LIFO":
            unfollow_list = list(reversed(unfollow_list))
        elif style == "RANDOM":
            random.shuffle(unfollow_list)

        if amount > len(unfollow_list):
            logger.info(
                "You have requested more amount: {} than {} of users "
                "available to unfollow"
                "~using available amount\n".format(amount, len(unfollow_list))
            )
            amount = len(unfollow_list)

        # unfollow loop
        try:
            sleep_counter = 0
            sleep_after = random.randint(8, 12)
            index = 0

            for person in unfollow_list:
                if unfollow_num >= amount:
                    logger.warning(
                        "--> Total unfollows reached it's amount given {}\n".format(
                            unfollow_num
                        )
                    )
                    break

                if jumps["consequent"]["unfollows"] >= jumps["limit"]["unfollows"]:
                    logger.warning(
                        "--> Unfollow quotient reached its peak!\t~leaving "
                        "Unfollow-Users activity\n"
                    )
                    break

                if sleep_counter >= sleep_after and sleep_delay not in [0, None]:
                    delay_random = random.randint(
                        ceil(sleep_delay * 0.85), ceil(sleep_delay * 1.14)
                    )
                    logger.info(
                        "Unfollowed {} new users  ~sleeping about {}\n".format(
                            sleep_counter,
                            "{} seconds".format(delay_random)
                            if delay_random < 60
                            else "{} minutes".format(
                                truncate_float(delay_random / 60, 2)
                            ),
                        )
                    )
                    sleep(delay_random)
                    sleep_counter = 0
                    sleep_after = random.randint(8, 12)
                    pass

                if person not in dont_include:
                    logger.info(
                        "Ongoing Unfollow [{}/{}]: now unfollowing '{}'...".format(
                            unfollow_num + 1, amount, person.encode("utf-8")
                        )
                    )

                    person_id = (
                        automatedFollowedPool["all"][person]["id"]
                        if person in automatedFollowedPool["all"].keys()
                        else False
                    )

                    # delay unfollowing of follow-backers
                    if delay_followbackers and unfollow_track != "nonfollowers":

                        followedback_status = automatedFollowedPool["all"][person][
                            "followedback"
                        ]
                        # if once before we set that flag to true
                        # now it is time to unfollow since
                        # time filter pass, user is now eligble to unfollow
                        if followedback_status is not True:

                            user_link = "https://www.instagram.com/{}/".format(person)
                            web_address_navigator(browser, user_link)
                            valid_page = is_page_available(browser, logger)

                            if valid_page and is_follow_me(browser, person):
                                # delay follow-backers with delay_follow_back.
                                time_stamp = (
                                    automatedFollowedPool["all"][person]["time_stamp"]
                                    if person in automatedFollowedPool["all"].keys()
                                    else False
                                )
                                if time_stamp not in [False, None]:
                                    try:
                                        time_diff = get_epoch_time_diff(
                                            time_stamp, logger
                                        )
                                        if time_diff is None:
                                            continue

                                        if (
                                            time_diff < delay_followbackers
                                        ):  # N days in seconds
                                            set_followback_in_pool(
                                                username,
                                                person,
                                                person_id,
                                                time_stamp,  # stay with original timestamp
                                                logger,
                                                logfolder,
                                            )
                                            # don't unfollow (for now) this follow backer !
                                            continue

                                    except ValueError:
                                        logger.error(
                                            "time_diff reading for user {} failed \n".format(
                                                person
                                            )
                                        )
                                        pass

                    try:
                        unfollow_state, msg = unfollow_user(
                            browser,
                            "profile",
                            username,
                            person,
                            person_id,
                            None,
                            relationship_data,
                            logger,
                            logfolder,
                        )
                    except BaseException as e:
                        logger.error("Unfollow loop error:  {}\n".format(str(e)))

                    post_unfollow_actions(browser, person, logger)

                    if unfollow_state is True:
                        unfollow_num += 1
                        sleep_counter += 1
                        # reset jump counter after a successful unfollow
                        jumps["consequent"]["unfollows"] = 0

                    elif msg == "jumped":
                        # will break the loop after certain consecutive jumps
                        jumps["consequent"]["unfollows"] += 1

                    elif msg in ["temporary block", "not connected", "not logged in"]:
                        # break the loop in extreme conditions to prevent
                        # misbehaviours
                        logger.warning(
                            "There is a serious issue: '{}'!\t~leaving "
                            "Unfollow-Users activity".format(msg)
                        )
                        break

                else:
                    # if the user in dont include (should not be) we shall
                    # remove him from the follow list
                    # if he is a white list user (set at init and not during
                    # run time)
                    if person in white_list:
                        delete_line_from_file(
                            "{0}{1}_followedPool.csv".format(logfolder, username),
                            person,
                            logger,
                        )
                        list_type = "whitelist"
                    else:
                        list_type = "dont_include"
                    logger.info(
                        "Not unfollowed '{}'!\t~user is in the list {}"
                        "\n".format(person, list_type)
                    )
                    index += 1
                    continue
        except BaseException as e:
            logger.error("Unfollow loop error:  {}\n".format(str(e)))
    else:
        logger.info(
            "Please select a proper unfollow method!  ~leaving unfollow activity\n"
        )

    return unfollow_num


def follow_user(browser, track, login, user_name, button, blacklist, logger, logfolder):
    """Follow a user either from the profile page or post page or dialog box"""
    # list of available tracks to follow in: ["profile", "post dialog"]

    # check action availability
    if quota_supervisor("follows") == "jump":
        return False, "jumped"

    if track in ["profile", "post"]:
        # if track == "profile":
        # check URL of the webpage, if it already is user's profile
        # page, then do not navigate to it again
        user_link = "https://www.instagram.com/{}/".format(user_name)
        web_address_navigator(browser, user_link)

        # find out CURRENT following status
        following_status, follow_button = get_following_status(
            browser, track, login, user_name, None, logger, logfolder
        )
        if following_status in ["Follow", "Follow Back"]:
            click_visibly(browser, follow_button)  # click to follow
            follow_state, msg = verify_action(
                browser, "follow", track, login, user_name, None, logger, logfolder
            )
            if follow_state is not True:
                return False, msg

        elif following_status in ["Following", "Requested"]:
            if following_status == "Following":
                logger.info("--> Already following '{}'!\n".format(user_name))

            elif following_status == "Requested":
                logger.info("--> Already requested '{}' to follow!\n".format(user_name))

            sleep(1)
            return False, "already followed"

        elif following_status in ["Unblock", "UNAVAILABLE"]:
            if following_status == "Unblock":
                failure_msg = "user is in block"

            elif following_status == "UNAVAILABLE":
                failure_msg = "user is inaccessible"

            else:
                # Trace the current status
                failure_msg = following_status

            logger.warning(
                "--> Couldn't follow '{}'!\t~{}".format(user_name, failure_msg)
            )
            return False, following_status

        elif following_status is None:
            sirens_wailing, emergency_state = emergency_exit(browser, login, logger)
            if sirens_wailing is True:
                return False, emergency_state

            else:
                logger.warning(
                    "--> Couldn't unfollow '{}'!\t~unexpected failure".format(user_name)
                )
                return False, "unexpected failure"
    elif track == "dialog":
        click_element(browser, button)
        sleep(3)

    # general tasks after a successful follow
    logger.info("--> Followed '{}'!".format(user_name.encode("utf-8")))
    Event().followed(user_name)
    update_activity(
        browser, action="follows", state=None, logfolder=logfolder, logger=logger
    )

    # get user ID to record alongside username
    user_id = get_user_id(browser, track, user_name, logger)

    logtime = datetime.now().strftime("%Y-%m-%d %H:%M")
    log_followed_pool(login, user_name, logger, logfolder, logtime, user_id)

    follow_restriction("write", user_name, None, logger)

    if blacklist["enabled"] is True:
        action = "followed"
        add_user_to_blacklist(
            user_name, blacklist["campaign"], action, logger, logfolder
        )

    # get the post-follow delay time to sleep
    naply = get_action_delay("follow")
    sleep(naply)

    return True, "success"


def scroll_to_bottom_of_followers_list(browser):
    browser.execute_script("window.scrollTo(0, document.body.scrollHeight)")
    return


def get_users_through_dialog_with_graphql(
    browser,
    login,
    user_name,
    amount,
    users_count,
    randomize,
    dont_include,
    blacklist,
    follow_times,
    simulation,
    channel,
    jumps,
    logger,
    logfolder,
    edge_followed_by,
):

    # TODO: simulation implmentation

    real_amount = amount
    followers_list = []

    if randomize and amount >= 3:
        # expanding the population for better sampling distribution
        amount = amount * 1.9
    try:
        user_id = browser.execute_script(
            "return window.__additionalData[Object.keys(window.__additionalData)[0]].data.graphql.user.id"
        )
    except WebDriverException:
        user_id = browser.execute_script(
            "return window._sharedData.entry_data.ProfilePage[0].graphql.user.id"
        )

    # There are two query hash, one for followers and following, ie:
    # t="c76146de99bb02f6415203be841dd25a",n="d04b0a864b4b54837c0d870b0e77e076"
    if edge_followed_by:
        # True: User requested session.follow_user_followers
        edge_type = "edge_followed_by"
    else:
        # False: User requested session.follow_user_following
        edge_type = "edge_follow"

    query_hash = get_query_hash(browser, logger, edge_followed_by)

    # check if hash is present
    if query_hash is None:
        logger.info("Unable to locate GraphQL query hash")
    else:
        logger.info("GraphQL query hash: [{}]".format(query_hash))

    graphql_query_URL = (
        "view-source:https://www.instagram.com/graphql/query/?query_hash={}".format(
            query_hash
        )
    )
    variables = {
        "id": str(user_id),
        "include_reel": "true",
        "fetch_mutual": "true",
        "first": 50,
    }
    url = "{}&variables={}".format(graphql_query_URL, str(json.dumps(variables)))

    web_address_navigator(browser, url)

    pre = browser.find_element_by_tag_name("pre")
    # set JSON object
    data = json.loads(pre.text)

    try:
        # get all followers or following of current page
        # edge_type: used to check followers or following in JSON
        #            "edge_followed_by" or "edge_follow"
        followers_page = data["data"]["user"][str(edge_type)]["edges"]
    except:
        # sometimes IG page displays a message that "Failed to Load. Retry"
        # so, instead of terminatig the App we will move to next step.
        # Cool down maybe ^.^
        # XPATH:
        #  class="gxNyb">Failed to Load.</p></div>
        # Marionette:
        #  {"value":"{\"message\": \"\", \"spam\": true, \"status\": \"fail\"}"}
        logger.error("JSON (1) cannot be loaded, moving on...")
        return [], []

    # iterate over page size and add users to the list
    for follower in followers_page:
        # get follower name
        followers_list.append(follower["node"]["username"])

    has_next_page = data["data"]["user"][str(edge_type)]["page_info"]["has_next_page"]

    while has_next_page and len(followers_list) <= amount:
        # server call interval
        sleep(random.randint(2, 6))

        # get next page reference
        end_cursor = data["data"]["user"][str(edge_type)]["page_info"]["end_cursor"]

        # url variables
        variables = {
            "id": str(user_id),
            "include_reel": "true",
            "fetch_mutual": "true",
            "first": 50,
            "after": end_cursor,
        }

        url = "{}&variables={}".format(graphql_query_URL, str(json.dumps(variables)))
        browser.get(url)
        pre = browser.find_element_by_tag_name("pre")

        # response to JSON object
        data = json.loads(pre.text)

        try:
            # get all followers of current page
            followers_page = data["data"]["user"][str(edge_type)]["edges"]
        except:
            logger.error("JSON (2) cannot be loaded, moving on...")
            return [], []

        # iterate over page size and add users to the list
        for follower in followers_page:
            # get follower name
            followers_list.append(follower["node"]["username"])

        # check if there is next page
        has_next_page = data["data"]["user"][str(edge_type)]["page_info"][
            "has_next_page"
        ]

        # simulation
        # TODO: this needs to be rewrited
        # if (
        #     simulation["enabled"] is True
        #     and simulation["percentage"] >= random.randint(1, 100)
        #     and (
        #         simulator_counter > random.randint(5, 17)
        #         or abort is True
        #         or total_list >= amount
        #         or sc_rolled == random.randint(3, 5)
        #     )
        #     and len(buttons) > 0
        # ):

        #     quick_amount = 1 if not total_list >= amount else random.randint(1, 4)

        #     for i in range(0, quick_amount):
        #         quick_index = random.randint(0, len(buttons) - 1)
        #         quick_button = buttons[quick_index]
        #         quick_username = dialog_username_extractor(quick_button)

        #         if quick_username and quick_username[0] not in simulated_list:
        #             if not pts_printed:
        #                 if total_list >= amount:
        #                     pts_printed = True

        #             logger.info("Simulated follow : {}".format(len(simulated_list) + 1))

        #             quick_follow = follow_through_dialog(
        #                 browser,
        #                 login,
        #                 quick_username,
        #                 quick_button,
        #                 quick_amount,
        #                 dont_include,
        #                 blacklist,
        #                 follow_times,
        #                 jumps,
        #                 logger,
        #                 logfolder,
        #             )
        #             if (quick_amount == 1 or i != (quick_amount - 1)) and (
        #                 not pts_printed or not abort
        #             ):
        #                 simulated_list.extend(quick_follow)

        #     simulator_counter = 0

    # shuffle it if randomize is enable
    if randomize:
        random.shuffle(followers_list)

    # get real amount
    followers_list = random.sample(followers_list, real_amount)

    for i, user in enumerate(followers_list):
        logger.info(
            "To be followed: [{}/{}/{}]".format(i + 1, len(followers_list), user)
        )

    return followers_list, []


def dialog_username_extractor(buttons):
    """Extract username of a follow button from a dialog box"""

    if not isinstance(buttons, list):
        buttons = [buttons]

    person_list = []
    for person in buttons:
        if person and hasattr(person, "text") and person.text:
            try:
                xpath = read_xpath(dialog_username_extractor.__name__, "person")
                element_by_xpath = person.find_element_by_xpath(xpath)
                elements_by_tag_name = element_by_xpath.find_elements_by_tag_name("a")[
                    0
                ].text

                if elements_by_tag_name == "":
                    elements_by_tag_name = element_by_xpath.find_elements_by_tag_name(
                        "a"
                    )[1].text

                person_list.append(elements_by_tag_name)
            except IndexError:
                print("how many?")
                pass  # Element list is too short to have a [1] element

    return person_list


def follow_through_dialog(
    browser,
    login,
    person_list,
    buttons,
    amount,
    dont_include,
    blacklist,
    follow_times,
    jumps,
    logger,
    logfolder,
):
    """Will follow username directly inside a dialog box"""
    if not isinstance(person_list, list):
        person_list = [person_list]

    if not isinstance(buttons, list):
        buttons = [buttons]

    person_followed = []
    followNum = 0

    try:
        for person, button in zip(person_list, buttons):
            if followNum >= amount:
                logger.info("--> Total follow number reached: {}".format(followNum))
                break

            elif jumps["consequent"]["follows"] >= jumps["limit"]["follows"]:
                logger.warning(
                    "--> Follow quotient reached its peak!\t~leaving "
                    "Follow-Through-Dialog activity\n"
                )
                break

            if person not in dont_include and not follow_restriction(
                "read", person, follow_times, logger
            ):
                follow_state, msg = follow_user(
                    browser,
                    "dialog",
                    login,
                    person,
                    button,
                    blacklist,
                    logger,
                    logfolder,
                )
                if follow_state is True:
                    # register this session's followed user for further
                    # interaction
                    person_followed.append(person)
                    followNum += 1
                    # reset jump counter after a successful follow
                    jumps["consequent"]["follows"] = 0

                elif msg == "jumped":
                    # will break the loop after certain consecutive jumps
                    jumps["consequent"]["follows"] += 1

            else:
                logger.info("Not followed '{}'  ~inappropriate user".format(person))

    except BaseException as e:
        logger.error(
            "Error occurred while following through dialog box:\n{}".format(str(e))
        )

    return person_followed


def get_given_user_followers(
    browser,
    login,
    user_name,
    amount,
    dont_include,
    randomize,
    blacklist,
    follow_times,
    simulation,
    jumps,
    logger,
    logfolder,
):
    """
    For the given username, follow their followers.

    :param browser: webdriver instance
    :param login:
    :param user_name: given username of account to follow
    :param amount: the number of followers to follow
    :param dont_include: ignore these usernames
    :param randomize: randomly select from users' followers
    :param blacklist:
    :param follow_times:
    :param logger: the logger instance
    :param logfolder: the logger folder
    :return: list of user's followers also followed
    """
    user_name = user_name.strip().lower()

    user_link = "https://www.instagram.com/{}/".format(user_name)
    web_address_navigator(browser, user_link)

    if not is_page_available(browser, logger):
        return [], []

    # check how many people are following this user.
    allfollowers, _ = get_relationship_counts(browser, user_name, logger)

    # skip early for no followers
    if not allfollowers:
        logger.info("'{}' has no followers".format(user_name))
        return [], []

    elif allfollowers < amount:
        logger.warning(
            "'{}' has less followers- {}, than the given amount of {}".format(
                user_name, allfollowers, amount
            )
        )

    # locate element to user's followers
    try:
        followers_link = browser.find_element_by_xpath(
            read_xpath(get_given_user_followers.__name__, "followers_link")
        )
        click_element(browser, followers_link)
        # update server calls
        update_activity(browser, state=None)

    except NoSuchElementException:
        logger.error("Could not find followers' link for '{}'".format(user_name))
        return [], []

    except BaseException as e:
        logger.error("`followers_link` error {}".format(str(e)))
        return [], []

    channel = "Follow"
    edge_followed_by = True
    person_list, simulated_list = get_users_through_dialog_with_graphql(
        browser,
        login,
        user_name,
        amount,
        allfollowers,
        randomize,
        dont_include,
        blacklist,
        follow_times,
        simulation,
        channel,
        jumps,
        logger,
        logfolder,
        edge_followed_by,
    )

    return person_list, simulated_list


def get_given_user_following(
    browser,
    login,
    user_name,
    amount,
    dont_include,
    randomize,
    blacklist,
    follow_times,
    simulation,
    jumps,
    logger,
    logfolder,
):
    """
    For the given username, follow who they follows.

    :param browser: webdriver instance
    :param login:
    :param user_name: given username of account to follow
    :param amount: the number of followers to follow
    :param dont_include: ignore these usernames
    :param randomize: randomly select from users' followers
    :param blacklist:
    :param follow_times:
    :param logger: the logger instance
    :param logfolder: the logger folder
    :return: list of user's following
    """
    user_name = user_name.strip().lower()

    user_link = "https://www.instagram.com/{}/".format(user_name)
    web_address_navigator(browser, user_link)

    if not is_page_available(browser, logger):
        return [], []

    #  check how many poeple are following this user.
    #  throw RuntimeWarning if we are 0 people following this user
    try:
        # allfollowing = format_number(
        #    browser.find_element_by_xpath(read_xpath(get_given_user_following.__name__,"all_following")).text)
        allfollowing = format_number(
            browser.find_element_by_xpath(
                read_xpath(get_given_user_following.__name__, "all_following")
            ).text
        )

    except NoSuchElementException:
        try:
            allfollowing = browser.execute_script(
                "return window.__additionalData[Object.keys(window.__additionalData)[0]].data."
                "graphql.user.edge_follow.count"
            )

        except WebDriverException:
            try:
                browser.execute_script("location.reload()")
                update_activity(browser, state=None)

                allfollowing = browser.execute_script(
                    "return window._sharedData."
                    "entry_data.ProfilePage[0]."
                    "graphql.user.edge_follow.count"
                )

            except WebDriverException:
                try:
                    topCount_elements = browser.find_elements_by_xpath(
                        read_xpath(
                            get_given_user_following.__name__, "topCount_elements"
                        )
                    )

                    if topCount_elements:
                        allfollowing = format_number(topCount_elements[2].text)
                    else:
                        logger.info(
                            "Failed to get following count of '{}'  ~empty "
                            "list".format(user_name)
                        )
                        allfollowing = None

                except (NoSuchElementException, IndexError):
                    logger.error(
                        "\nError occured during getting the following count "
                        "of '{}'\n".format(user_name)
                    )
                    return [], []

    # skip early for no followers
    if not allfollowing:
        logger.info("'{}' has no any following".format(user_name))
        return [], []

    elif allfollowing < amount:
        logger.warning(
            "'{}' has less following- {} than the desired amount of {}".format(
                user_name, allfollowing, amount
            )
        )

    try:
        following_link = browser.find_elements_by_xpath(
            read_xpath(get_given_user_following.__name__, "following_link").format(
                user_name
            )
        )
        click_element(browser, following_link[0])
        # update server calls
        update_activity(browser, state=None)

    except NoSuchElementException:
        logger.error("Could not find following's link for '{}'".format(user_name))
        return [], []

    except BaseException as e:
        logger.error("`following_link` error {}".format(str(e)))
        return [], []

    channel = "Follow"
    edge_followed_by = False
    person_list, simulated_list = get_users_through_dialog_with_graphql(
        browser,
        login,
        user_name,
        amount,
        allfollowing,
        randomize,
        dont_include,
        blacklist,
        follow_times,
        simulation,
        channel,
        jumps,
        logger,
        logfolder,
        edge_followed_by,
    )

    return person_list, simulated_list


def dump_follow_restriction(profile_name, logger, logfolder):
    """Dump follow restriction data to a local human-readable JSON"""

    conn = None

    try:
        # get a DB and start a connection
        db, id = get_database()
        conn = sqlite3.connect(db)

        with conn:
            conn.row_factory = sqlite3.Row
            cur = conn.cursor()

            cur.execute(
                "SELECT * FROM followRestriction WHERE profile_id=:var", {"var": id}
            )
            data = cur.fetchall()

        if data:
            # get the existing data
            filename = "{}followRestriction.json".format(logfolder)
            if os.path.isfile(filename):
                with open(filename) as followResFile:
                    current_data = json.load(followResFile)
            else:
                current_data = {}

            # pack the new data
            follow_data = {user_data[1]: user_data[2] for user_data in data or []}
            current_data[profile_name] = follow_data

            # dump the fresh follow data to a local human readable JSON
            with open(filename, "w") as followResFile:
                json.dump(current_data, followResFile)

    except Exception as exc:
        logger.error(
            "Pow! Error occurred while dumping follow restriction data to a "
            "local JSON:\n\t{}".format(str(exc).encode("utf-8"))
        )

    finally:
        if conn:
            # close the open connection
            conn.close()


def follow_restriction(operation, username, limit, logger):
    """Keep track of the followed users and help avoid excessive follow of
    the same user"""

    conn = None

    try:
        # get a DB and start a connection
        db, profile_id = get_database()
        conn = sqlite3.connect(db)

        with conn:
            conn.row_factory = sqlite3.Row
            cur = conn.cursor()

            cur.execute(
                "SELECT * FROM followRestriction WHERE profile_id=:id_var "
                "AND username=:name_var",
                {"id_var": profile_id, "name_var": username},
            )
            data = cur.fetchone()
            follow_data = dict(data) if data else None

            if operation == "write":
                if follow_data is None:
                    # write a new record
                    cur.execute(
                        "INSERT INTO followRestriction (profile_id, "
                        "username, times) VALUES (?, ?, ?)",
                        (profile_id, username, 1),
                    )
                else:
                    # update the existing record
                    follow_data["times"] += 1
                    sql = (
                        "UPDATE followRestriction set times = ? WHERE "
                        "profile_id=? AND username = ?"
                    )
                    cur.execute(sql, (follow_data["times"], profile_id, username))

                # commit the latest changes
                conn.commit()

            elif operation == "read":
                if follow_data is None:
                    return False

                elif follow_data["times"] < limit:
                    return False

                else:
                    exceed_msg = "" if follow_data["times"] == limit else "more than "
                    logger.info(
                        "--> {} has already been followed {}{} times".format(
                            username, exceed_msg, str(limit)
                        )
                    )
                    return True

    except Exception as exc:
        logger.error(
            "Dap! Error occurred with follow Restriction:\n\t{}".format(
                str(exc).encode("utf-8")
            )
        )

    finally:
        if conn:
            # close the open connection
            conn.close()


def unfollow_user(
    browser,
    track,
    username,
    person,
    person_id,
    button,
    relationship_data,
    logger,
    logfolder,
):
    """Unfollow a user either from the profile or post page or dialog box"""
    # list of available tracks to unfollow in: ["profile", "post dialog]
    # check action availability
    if quota_supervisor("unfollows") == "jump":
        return False, "jumped"

    if track in ["profile", "post"]:
        # Method of unfollowing from a user's profile page or post page
        if track == "profile":
            user_link = "https://www.instagram.com/{}/".format(person)
            web_address_navigator(browser, user_link)

        # find out CURRENT follow status
        following_status, follow_button = get_following_status(
            browser, track, username, person, person_id, logger, logfolder
        )

        if following_status in ["Following", "Requested"]:
            click_element(browser, follow_button)  # click to unfollow
            sleep(4)  # TODO: use explicit wait here
            confirm_unfollow(browser)
            unfollow_state, msg = verify_action(
                browser,
                "unfollow",
                track,
                username,
                person,
                person_id,
                logger,
                logfolder,
            )
            if unfollow_state is not True:
                return False, msg

        elif following_status in ["Follow", "Follow Back"]:
            logger.info(
                "--> Already unfollowed '{}'! or a private user that "
                "rejected your req".format(person)
            )
            post_unfollow_cleanup(
                ["successful", "uncertain"],
                username,
                person,
                relationship_data,
                person_id,
                logger,
                logfolder,
            )
            return False, "already unfollowed"

        elif following_status in ["Unblock", "UNAVAILABLE"]:
            if following_status == "Unblock":
                failure_msg = "user is in block"

            elif following_status == "UNAVAILABLE":
                failure_msg = "user is inaccessible"

            else:
                # Trace the current status
                failure_msg = following_status

            logger.warning(
                "--> Couldn't unfollow '{}'!\t~{}".format(person, failure_msg)
            )
            post_unfollow_cleanup(
                "uncertain",
                username,
                person,
                relationship_data,
                person_id,
                logger,
                logfolder,
            )
            return False, following_status

        elif following_status is None:
            sirens_wailing, emergency_state = emergency_exit(browser, username, logger)
            if sirens_wailing is True:
                return False, emergency_state

            else:
                logger.warning(
                    "--> Couldn't unfollow '{}'!\t~unexpected failure".format(person)
                )
                return False, "unexpected failure"
    elif track == "dialog":
        # Method of unfollowing from a dialog box

        click_element(browser, button)
        sleep(4)  # TODO: use explicit wait here
        confirm_unfollow(browser)

    # general tasks after a successful unfollow
    logger.info("--> Unfollowed '{}'!".format(person))
    Event().unfollowed(person)
    update_activity(
        browser, action="unfollows", state=None, logfolder=logfolder, logger=logger
    )
    post_unfollow_cleanup(
        "successful", username, person, relationship_data, person_id, logger, logfolder
    )

    # get the post-unfollow delay time to sleep
    naply = get_action_delay("unfollow")
    sleep(naply)

    return True, "success"


def confirm_unfollow(browser):
    """Deal with the confirmation dialog boxes during an unfollow"""
    attempt = 0

    while attempt < 3:
        try:
            attempt += 1
            button_xp = read_xpath(
                confirm_unfollow.__name__, "button_xp"
            )  # "//button[contains(
            # text(), 'Unfollow')]"
            unfollow_button = browser.find_element_by_xpath(button_xp)

            if unfollow_button.is_displayed():
                click_element(browser, unfollow_button)
                sleep(2)
                break

        except (ElementNotVisibleException, NoSuchElementException) as exc:
            # prob confirm dialog didn't pop up
            if isinstance(exc, ElementNotVisibleException):
                break

            elif isinstance(exc, NoSuchElementException):
                sleep(1)


def post_unfollow_cleanup(
    state, username, person, relationship_data, person_id, logger, logfolder
):
    """Casual local data cleaning after an unfollow"""
    if not isinstance(state, list):
        state = [state]

    delete_line_from_file(
        "{0}{1}_followedPool.csv".format(logfolder, username), person, logger
    )

    if "successful" in state:
        if person in relationship_data[username]["all_following"]:
            relationship_data[username]["all_following"].remove(person)

    if "uncertain" in state:
        # this user was found in our unfollow list but currently is not
        # being followed
        logtime = get_log_time()
        log_uncertain_unfollowed_pool(
            username, person, logger, logfolder, logtime, person_id
        )
        # take a generic 3 seconds of sleep per each uncertain unfollow
        sleep(3)

    # save any unfollowed person
    log_record_all_unfollowed(username, person, logger, logfolder)


def get_buttons_from_dialog(dialog, channel):
    """Gets buttons from the `Followers` or `Following` dialog boxes"""

    buttons = None

    if channel == "Follow":
        # get follow buttons. This approach will find the follow buttons and
        # ignore the Unfollow/Requested buttons.
        buttons = dialog.find_elements_by_xpath(
            read_xpath(get_buttons_from_dialog.__name__, "follow_button")
        )

    elif channel == "Unfollow":
        buttons = dialog.find_elements_by_xpath(
            read_xpath(get_buttons_from_dialog.__name__, "unfollow_button")
        )

    return buttons


def get_user_id(browser, track, username, logger):
    """Get user's ID either from a profile page or post page"""
    user_id = "unknown"

    if track != "dialog":  # currently do not get the user ID for follows
        # from 'dialog'
        user_id = find_user_id(browser, track, username, logger)

    return user_id


def verify_action(
    browser, action, track, username, person, person_id, logger, logfolder
):
    """Verify if the action has succeeded"""
    # currently supported actions are follow & unfollow

    retry_count = 0
    post_action_text_fail = None
    post_action_text_correct = None

    if action in ["follow", "unfollow"]:

        # assuming button_change testing is relevant to those actions only
        button_change = False

        if action == "follow":
            post_action_text_correct = ["Following", "Requested"]
            post_action_text_fail = ["Follow", "Follow Back", "Unblock"]

        elif action == "unfollow":
            post_action_text_correct = ["Follow", "Follow Back", "Unblock"]
            post_action_text_fail = ["Following", "Requested"]

        while True:

            # count retries at beginning
            retry_count += 1

            # find out CURRENT follow status (this is safe as the follow button is before others)
            following_status, follow_button = get_following_status(
                browser, track, username, person, person_id, logger, logfolder
            )
            if following_status in post_action_text_correct:
                button_change = True
            elif following_status in post_action_text_fail:
                button_change = False
            else:
                logger.error(
                    "Hey! Last {} is not verified out of an unexpected "
                    "failure!".format(action)
                )
                return False, "unexpected"

            if button_change:
                break
            else:
                if retry_count == 1:
                    reload_webpage(browser)
                    sleep(4)

                elif retry_count == 2:
                    # handle it!
                    # try to do the action one more time!
                    click_visibly(browser, follow_button)

                    if action == "unfollow":
                        confirm_unfollow(browser)

                    sleep(4)
                elif retry_count == 3:
                    user_link = "https://www.instagram.com/{}/".format(person)
                    web_address_navigator(browser, user_link)
                    follow_button_xp = read_xpath(
                        "get_following_status", "follow_button_XP"
                    )
                    button = browser.find_element_by_xpath(follow_button_xp)
                    try:
                        button.click()
                    except:
                        return False, "unexpected"
                    sleep(random.randint(4, 7))
                    return True, "success"

                elif retry_count == 4:
                    logger.warning(
                        "Last {0} is not verified."
                        "\t~'{1}' might be temporarily blocked "
                        "from {0}ing\n".format(action, username)
                    )
                    sleep(210)
                    return False, "temporary block"

        logger.info("Last {} is verified after reloading the page!".format(action))

    return True, "success"


def post_unfollow_actions(browser, person, logger):
    pass


def get_follow_requests(browser, amount, sleep_delay, logger, logfolder):
    """Get follow requests from instagram access tool list"""

    user_link = "https://www.instagram.com/accounts/access_tool/current_follow_requests"
    web_address_navigator(browser, user_link)

    list_of_users = []
    view_more_button_exist = True
    view_more_clicks = 0
    view_more_button = None

    while (
        len(list_of_users) < amount
        and view_more_clicks < 750
        and view_more_button_exist
    ):
        sleep(4)
        list_of_users = browser.find_elements_by_xpath(
            read_xpath(get_follow_requests.__name__, "list_of_users")
        )

        if len(list_of_users) == 0:
            logger.info("There are not outgoing follow requests")
            break

        try:
            view_more_button = browser.find_element_by_xpath(
                read_xpath(get_follow_requests.__name__, "view_more_button")
            )
        except NoSuchElementException:
            view_more_button_exist = False

        if view_more_button_exist:
            logger.info(
                "Found '{}' outgoing follow requests, Going to ask for more...".format(
                    len(list_of_users)
                )
            )
            click_element(browser, view_more_button)
            view_more_clicks += 1

    users_to_unfollow = []

    for user in list_of_users:
        users_to_unfollow.append(user.text)
        if len(users_to_unfollow) == amount:
            break

    logger.info(
        "Found '{}' outgoing follow requests '{}'".format(
            len(users_to_unfollow), users_to_unfollow
        )
    )

    return users_to_unfollow


def set_followback_in_pool(username, person, person_id, logtime, logger, logfolder):
    # first we delete the user from pool
    delete_line_from_file(
        "{0}{1}_followedPool.csv".format(logfolder, username), person, logger
    )

    # return the username with new timestamp
    log_followed_pool(username, person, logger, logfolder, logtime, person_id)


def refresh_follow_time_in_pool(
    username, person, person_id, extra_secs, logger, logfolder
):
    # set the new time to now plus extra delay
    logtime = (datetime.now() + timedelta(seconds=extra_secs)).strftime(
        "%Y-%m-%d %H:%M"
    )

    # first we delete the user from pool
    delete_line_from_file(
        "{0}{1}_followedPool.csv".format(logfolder, username), person, logger
    )

    # return the username with new timestamp
    log_followed_pool(username, person, logger, logfolder, logtime, person_id)
