import os
from typing import Literal

import pandas as pd
import requests
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
from tqdm import tqdm

# Utils


## Download a file
def download(url="https://", filename="", headers: dict = {}):
    """Download a file with progress bar.

    Parameters
    ----------
    url : string
        The url of the file to download.
    filename : string
        The local filename of the file to download.
    headers : dictionary
        The headers of the download's request.
    """

    response = requests.get(
        url,
        headers=headers,
        data={"auth_token": headers.get("auth_token")},
        stream=True,
    )
    response.raise_for_status()  # check if there is no error
    total = int(response.headers.get("content-length", 0))
    with (
        open(filename, "wb") as file,
        tqdm(
            desc=filename,
            total=total,
            unit="o",
            unit_scale=True,
            unit_divisor=1024,
        ) as bar,
    ):
        for data in response.iter_content(chunk_size=1024):
            size = file.write(data)
            bar.update(size)


# Upload a file
def upload(filepath, comment, url, headers):
    """Upload a file with progress bar.

    Parameters
    ----------
    filepath : string
        The local filepath of the file to upload.
    comment : string
        The comment for the file to upload.
    url : string
        The url of the file to upload.
    headers : dictionary
        The headers of the upload's request.

    Returns
    -------
    headers : dictionary | json
        The response of the upload's request.
    """

    filename = (os.sep).join(filepath.split(os.sep)[-2:])
    encoder = MultipartEncoder(
        {"file": (filename, open(filepath, "rb"), "text/plain"), "comment": comment}
    )

    with tqdm(
        desc=f"Submit {filename}",
        total=encoder.len,
        ncols=100,
        unit="o",
        unit_scale=True,
        unit_divisor=1024,
    ) as progress_bar:

        multipart_monitor = MultipartEncoderMonitor(
            encoder,
            lambda monitor: progress_bar.update(monitor.bytes_read - progress_bar.n),
        )
        headers = {
            **headers,
            "Content-Type": multipart_monitor.content_type,
        }

        response = requests.post(
            url,
            data=multipart_monitor,
            params={"auth_token": headers["auth_token"]},
            headers=headers,
        )
    return response


## Print
### Challenges
def print_challenges(challenges_data):
    """Formated print the Zindi's challenge as table.

    Parameters
    ----------
    challenges_data : dictionary | json
        The json's response of the request to get informations about the challenges.
    """

    n_challenges = challenges_data.shape[0]  # total count of retreived challenges
    print("_" * 130)
    print(
        "|{:^5}|{:^14.14}|{:^18.18}|{:^20.20}|{:^10}".format(
            "",
            "",
            "",
            "",
            "",
        )
    )
    print(
        "|{:^5}|{:^14.14}|{:^18.18}|{:^20.20}|{:^10}".format(
            "index",
            "challenge",
            "problem",
            "reward",
            "id",
        )
    )
    print(
        "|{:^5}|{:^14.14}|{:^18.18}|{:^20.20}|{:^10}".format(
            "",
            "",
            "",
            "",
            "",
        )
    )
    # for data in challenges_data:
    for i in range(n_challenges):
        data = dict(challenges_data.iloc[i])  # data - each challenge, one after another
        reward = data["reward"]  # Challenge's reward for top challengers
        id = data["id"][:50] + "..."  # Challenge's id
        problem = (
            "" if len(data["type_of_problem"]) == 0 else data["type_of_problem"][0]
        )  # category of the challenge's problem
        kind = (
            "Hack" if data["kind"] == "hackathon" else "Compet"
        )  # simple challenge's kind
        visibility = (
            "Public" if not data["secret_code_required"] else "Private"
        )  # Challenge's visibility
        challenge = (
            f"{visibility} {kind}"  # general challenge's kind - join visibility & kind
        )
        print("-" * 130)
        print(
            "|{:^5}|{:^14.14}|{:^18.18}|{:^20.20}| {:10}".format(
                i,
                challenge,
                problem,
                reward,
                id,
            )
        )
    print(f"{'_'*130}\n\n")


## Print
### Leaderboard
def print_lb(challengers_data, user_rank):
    """Formated print the Zindi's challenge leaderboard as table.

    Parameters
    ----------
    challengers_data : dictionary | json
        The json's response of the request to get informations about the leaderboard.
    user_rank : int
        The rank of the user on the leaderboard of a challenge.
    """

    print("_" * 130)
    print(
        "|{:^6}|{:^20}|{:^44}|{:^12}|{:^12}".format(
            "",
            "",
            "",
            "",
            "",
        )
    )
    print(
        "|{:^6}|{:^20}|{:^44}|{:^12}|{:^12}".format(
            "rank",
            "score",
            "name",
            "counter",
            "last_submission",
        )
    )
    print(
        "|{:^6}|{:^20}|{:^44}|{:^12}|{:^12}".format(
            "",
            "",
            "",
            "",
            "",
        )
    )
    # for data in challengers_data:
    for i in range(len(challengers_data)):
        data = challengers_data[i]  # data - each row of the leaderboard
        try:
            score = (
                data["best_private_score"]
                if "best_private_score" in data
                else data["best_public_score"]
            )  # Best submission's score
            rank = (
                data["private_rank"] if "private_rank" in data else data["public_rank"]
            )  # Rank on leaderboard
            name = (
                data["user"]["username"]
                if "user" in data
                else f"TEAM - {data['team']['title']}"
            )  # Name of user or team
            n_submission = data["submission_count"]
            last_submission = (
                data["best_private_submitted_at"]
                if "best_private_submitted_at" in data
                else data["best_public_submitted_at"]
            )
            check = str(last_submission) == str(
                None
            )  # conditioin to set last_submission value
            last_submission = (
                ""
                if check
                else pd.to_datetime(str(last_submission)).strftime("%d %B %Y, %H:%M")
            )
            # Check rank to exclude not yet active challengers
            if rank is not None:
                if (
                    rank == user_rank
                ):  # Check rank to mark my position on the leaderboard
                    name = f"{name} 🟢"
                print("-" * 130)
                print(
                    "|{:^6}|{:^20.20}|{:^44.44}|{:^12.12}|{:^12}".format(
                        str(rank),
                        str(score),
                        str(name),
                        str(n_submission),
                        str(last_submission),
                    )
                )
            else:
                break  # all the next zindians haven't made a correct submission yet
        except Exception as e:
            print(e)
    print(f"{'_'*130}\n\n")


## Print
### Submission-board
def print_submission_board(submissions_data):
    """Formated print the Zindi's challenge submission-board as table.

    Parameters
    ----------
    submissions_data : dictionary | json
        The json's response of the request to get informations about the submission-board of a challenge.
    """

    print("_" * 130)
    print(
        "|{:^6}|{:^10}|{:^18}|{:^16}|{:^30} |{:^25}".format(
            "",
            "",
            "",
            "",
            "",
            "",
        )
    )
    print(
        "|{:^6}|{:^10}|{:^18}|{:^16}|{:^30} |{:^25}".format(
            "status",
            "id",
            "date",
            "score",
            "filename",
            "comment",
        )
    )
    print(
        "|{:^6}|{:^10}|{:^18}|{:^16}|{:^30} |{:^25}".format(
            "",
            "",
            "",
            "",
            "",
            "",
        )
    )

    for data in submissions_data:
        filename = data["filename"]  # Submission's filename
        date = pd.to_datetime(data["created_at"]).strftime(
            "%d %b %Y, %H:%M"
        )  # Date of submission
        id = data["id"]  # submission's id

        if data["status"] in ["successful", "initial"]:
            status = "🟢"  # Status - for valid of the submission
            score = (
                data["private_score"]
                if "private_score" in data
                else data["public_score"]
            )  # Score of the submission
            score = (
                "In processing" if score is None else score
            )  # Show a message when processing of the score is not yet finished
            comment = (
                "" if data["comment"] is None else data["comment"]
            )  # Comment of the submission
        else:
            score = "-"  # Score of the submission : "-" for wrong submission
            status = "🔴"  # Status - for wrong of the submission
            comment = (
                "" if data["status_description"] is None else data["status_description"]
            )  # Submission error description
        print("-" * 130)
        try:
            print(
                "|{:^5}|{:^10}|{:^12}| {:^14.14} |{:30.30} |{:40.40}".format(
                    status,
                    id,
                    date,
                    str(score),
                    filename,
                    comment,
                )
            )
        except:
            print(
                "|{:^5}|{:^10}|{:^12}| {:^14.14} |{:30.30} |{:40.40}".format(
                    status,
                    id,
                    date,
                    "",
                    filename,
                    "Oop there is an unknown bug, sorry !",
                )
            )
    print(f"{'_'*130}\n\n")


## Join challenge
def join_challenge(url, headers, code=False, to_print=True):
    """Join a Zindi challenge.

    Parameters
    ----------
    url : string
        The url of the selected challenge participations endpoint.
    headers : dictionary
        The headers of the request (should include 'auth-token').
    code : bool, default=False
        Whether a secret code is required.
    to_print : bool, default=True
        Whether to print interactive status messages.
    """

    if not code:
        response = requests.post(url=url, headers=headers)
    else:
        secret_code = input("Enter the secret code to join the challenge.\n>>")
        params = {"secret_code": secret_code}
        response = requests.post(url=url, headers=headers, params=params)

    response = response.json()["data"]
    if "errors" in response:  # raise error if request failed
        error = response["errors"]["message"]
        if error in ["already in", "Great news! You've already joined the competition"]:
            return {"joined": True, "message": error}
        elif error == "This competition requires a secret code to join.":
            return join_challenge(url, headers, code=True, to_print=to_print)
        else:
            msg_error = f"\n[ 🔴 ] {error}\n"
            raise Exception(msg_error)
    else:  # else print success message
        if "ids" in response:
            if to_print:
                print("\n[ 🟢 ] Welcome for the first time to this challenge.\n")
        else:
            if to_print:
                print("\n[ 🟢 ] {response}.\n")
    return {"joined": True, "message": response}


## Get available challenges
def get_challenges(
    query: str = None,
    kind: Literal["competition", "hackathon"] = "competition",
    reward: Literal["prize", "points", "knowledge"] = None,
    active: bool = True,
    url: str = "",
    headers: dict = {},
    per_page: int = 20,
):
    """Get the available Zindi's challenges using filter options.

    Parameters
    ----------
    query : str, default=None
        Text search query to filter challenges by name.
    kind : {'competition', 'hackathon'}, default='competition'
        The kind of the challenges.
    reward : {'prize', 'points', 'knowledge'}, default=None
        The reward type of the challenges. None means all rewards.
    active : bool, default=True
        Whether to show only active challenges.
    url : string
        The base API url for competitions.
    headers : dictionary
        The headers of the request.
    per_page : int, default=20
        Number of challenges to retrieve per page.

    Returns
    -------
    challenges_data : pd.DataFrame
        The response of the request to get informations about the available challenges.
    """
    if kind is not None and kind.lower() not in ["competition", "hackathon"]:
        kind = "competition"  # default value
    if reward is not None and reward.lower() not in ["prize", "points", "knowledge"]:
        reward = ""  # default value
    if not isinstance(active, bool):
        active = ""  # default value

    to_show_challenge_data = [
        "id",
        "kind",
        "subtitle",
        "reward",
        "type_of_problem",
        "data_type",
        "secret_code_required",
        "sealed",
    ]
    challenges_data = pd.DataFrame()

    # Build query parameters
    params = {
        "page": 0,
        "per_page": per_page,
        "active": str(active).lower(),
        "kind[]": kind,
    }
    if query:
        params["query"] = query
    if reward and reward.lower() in ["prize", "points", "knowledge"]:
        params["prize"] = reward.lower()

    # request
    response = requests.get(url, headers=headers, params=params)
    response = response.json()["data"]
    try:  # raise error if request failed
        print(response["errors"])
    except:  # else go on in processing
        challenges_data = pd.DataFrame(response)
        if not challenges_data.empty:
            # extract relevant columns and print the recap table
            challenges_data = challenges_data[to_show_challenge_data]
    return challenges_data


## challenge index selector
def challenge_idx_selector(n_challenges):
    """Get on the keyboard the index of the challenge tha the user want to participate in.

    Parameters
    ----------
    n_challenges : integer
        The number of retrieved challenges.

    Returns
    -------
    challenge_index : int
        The index of the challenge selected by the user.
    """

    challenge_index = -1

    while True:
        user_input = input(
            "Type the index of the challenge you want to select or 'q' to exit.\n>>"
        )
        try:  # user_input must be a valid integer or 'q' else we stay in the infinite loop
            user_input = int(user_input.strip())

            if (user_input >= 0) and (user_input < n_challenges):
                challenge_index = user_input
                return challenge_index
            else:
                raise Exception()
        except:
            if (
                isinstance(user_input, str) and user_input.lower().strip() == "q"
            ):  # to stop selection
                return challenge_index
            else:
                print("\n[ 🔴 ] Please enter a correct challenge index.\n")


##  Info about the challenges user participate in
def participations(challenge_id, headers):
    """Check if user is in team for a the Zindi's challenges.

    Parameters
    ----------
    challenge_id : string
        The id of the selected challenge.
    headers : dictionary
        The headers of the request to participate in a challenge.

    Returns
    -------
    challenges_data : pd.DataFrame
        The response of the request to get informations about the available challenges.
    """
    url = "https://api.zindi.africa/v1/participations"
    response = requests.get(url, headers=headers)
    response.raise_for_status()  # check if there is no error
    response = response.json()["data"]
    team_id = response[challenge_id]["team_id"]
    return team_id


## Info about user position on lb
def user_on_lb(challengers_data, challenge_id, username, headers):
    """Get rank of user on the leaderboard for a the Zindi's challenges.

    Parameters
    ----------
    challengers_data : dictionary | json
        The json's response of the request to get informations about the leaderboard.
    challenge_id : string
        The id of the selected challenge.
    username : string
        The username of the user.
    headers : dictionary
        The headers of the request to participate in a challenge.

    Returns
    -------
    user_rank : int
        The rank of the user on the leaderboard of a challenge.
    """
    df_lb = pd.DataFrame(challengers_data)  # DataFrame verion of the leaderboard
    df_lb = df_lb[
        ~df_lb["public_rank"].isna()
    ]  # filter to use only leaderboard part with the active challengers
    try:
        team_id = participations(challenge_id=challenge_id, headers=headers)
        # Simple user
        if not team_id:
            user = df_lb[
                df_lb["user"]
                .astype(str)
                .str.contains(
                    username,
                )
            ]  # na=False, case=True, regex=False
            user_index = user.index.values[0].astype(int)
        # Team
        else:
            my_team = df_lb[
                df_lb["team"]
                .astype(str)
                .str.contains(
                    team_id,
                )
            ]
            user_index = my_team.index.values[0].astype(int)
        user_rank = user_index + 1
    except:
        user_rank = 0  # rank initialization if user is not yet active for the challenge
    return user_rank
