from time import sleep

from actions_logging.app_logging import logger
from common.common import append_to_partitioned_text, raise_with_context
from github.constants import GH_URL
from github.env import exit_on_error_and_write_summary
from github.github_apis import get_all_users, get_user_by_username
from slack_wrapper.constants import MAX_MESSAGE_LENGTH
from slack_wrapper.message import send_message_on_slack
from slack_wrapper.users import get_slack_user_by_email


def get_slack_user_by_github_user(github_user: str) -> dict | None:
    """
    Get the Slack user linked to a GitHub user by their email address.

    :param github_user: The GitHub username to look up.
    :return: A dictionary with Slack user details if found, otherwise None.
    """
    try:
        user_details = get_user_by_username(github_user)
        if user_details:
            logger.debug(f"User {github_user} found in GitHub API")
            user_email = user_details.get("email")
            if user_email:
                logger.debug(f"User {github_user} has public email {user_email}")
                slack_user = get_slack_user_by_email(user_email)
                if slack_user:
                    logger.debug(f"User {github_user} is linked to Slack user {slack_user}")
                    return slack_user
                else:
                    logger.warning(
                        f"Slack user cannot be found for GitHub user {github_user} "
                        f"(email {user_email} set in profile not linked to any Slack user)."
                    )
                    return None
            else:
                logger.warning(
                    f"Slack user cannot be found for GitHub user {github_user} "
                    "(has no publiclt exposed email set in profile)."
                )
        else:
            logger.warning(f"Slack user cannot be found for GitHub user {github_user} (not found in GitHub API).")
            return None
    except Exception as e:
        raise_with_context(e, RuntimeError, "Failed to get Slack user by GitHub user")


def get_github_users_unlinkable_to_slack_users(page_size: int = 100) -> dict:
    """
    Get a list of GitHub users that cannot be linked to Slack users.

    :param page_size: The number of users to fetch per request.
    :return: A dictionary with users that cannot be linked to Slack users.
    """
    try:
        # Dictionary to store users that cannot be linked to Slack users
        # The keys are the reasons for the unlinkability and the values are lists of users
        reasons = {
            "not_found_in_github_api": "Users not found in GitHub API",
            "no_public_email": "Users with no publicly exposed email address",
            "email_address_mismatch": "Users with publicly exposed email address that is not set on a Slack user",
        }

        unlinkable_users = {
            reasons["not_found_in_github_api"]: [],
            reasons["no_public_email"]: [],
            reasons["email_address_mismatch"]: [],
        }

        all_github_users = get_all_users(page_size=page_size)

        cnt = len(all_github_users)
        logger.info(f"Found {cnt} GitHub users in the organization")

        for i, user in enumerate(all_github_users, start=1):
            try:
                user_login = user["login"]
                logger.debug(f"Checking user {user_login} ({i}/{cnt}): {user}")
                user_details = get_user_by_username(user_login)
                if user_details:
                    logger.debug(f"User {user_login} found in GitHub API: {user_details}")
                    user_email = user_details.get("email")
                    if user_email:
                        logger.info(f"User {user_login} has public email {user_email}")
                        slack_user = get_slack_user_by_email(user_email)
                        if slack_user:
                            logger.info(f"User {user_login} is linked to Slack user {slack_user['id']}")
                        else:
                            logger.info(f"User {user_login} with email {user_email} is not linked to any Slack user")
                            unlinkable_users[reasons["email_address_mismatch"]].append(user_details)
                    else:
                        logger.info(f"User {user_login} has no email, thus cannot be linked to a Slack user")
                        unlinkable_users[reasons["no_public_email"]].append(user_details)
                else:
                    logger.warning(
                        f"User {user_login} ({i}/{cnt}) was listed in org users, but not found as user in GitHub API"
                    )
                    unlinkable_users[reasons["not_found_in_github_api"]].append(user_details)
            except Exception as e:
                logger.error(f"Error processing user {user} ({i}/{cnt}): {e}")
                unlinkable_users[reasons["not_found_in_github_api"]].append(user)

        for reason in unlinkable_users.keys():
            logger.info(f"{reason}: {len(unlinkable_users[reason])}")

        return unlinkable_users
    except Exception as e:
        raise_with_context(e, RuntimeError, "Failed to get users with private emails")


def publish_list_of_unlinkable_users(slack_channel: str = "#list-of-github-users-unlinkable-to-slack-users"):
    """
    Publish a list of GitHub users that cannot be linked to Slack users to a Slack channel.
    The list is divided into multiple messages if it exceeds the maximum message size.
    Each message contains a header with the reason for the list of users, and each user is listed with their login,
    name, and email if available.

    :param slack_channel: The Slack channel to send the message, prefixed with #.
    """

    def compose_user_line(user: dict) -> str:
        user_line = f"\n• <{GH_URL}/{user['login']}|{user['login']}>"
        user_name = user.get("name") or None
        user_email = user.get("email") or None
        if user_name or user_email:
            user_line += " ("
            if user_name:
                user_line += user_name
                user_line += " " if user_email else ""
            if user_email:
                user_line += user_email
            user_line += ")"
        return user_line

    try:
        messages_to_send = []
        text_to_end_nonlast_parts = (
            f"Breaking the message to avoid Slack message size limit of {MAX_MESSAGE_LENGTH} chars"
        )
        text_to_start_nonfirst_parts = "Continuing the list of users..."

        logger.info("Getting GitHub users unlinkable to Slack users")
        unlinkable_users = get_github_users_unlinkable_to_slack_users()

        for reason in unlinkable_users.keys():
            if len(unlinkable_users[reason]) > 0:
                # Add a header with reason for the list of users
                append_to_partitioned_text(
                    messages_to_send,
                    f"\n\n{reason}:",
                    MAX_MESSAGE_LENGTH,
                    text_to_end_nonlast_parts,
                    text_to_start_nonfirst_parts,
                )

                # Add each user to the message, with name and email if available
                for user in unlinkable_users[reason]:
                    # Append the line to the message
                    user_line = compose_user_line(user)
                    append_to_partitioned_text(
                        messages_to_send,
                        user_line,
                        MAX_MESSAGE_LENGTH,
                        text_to_end_nonlast_parts,
                        text_to_start_nonfirst_parts,
                    )

                # Adding a summary line to the message
                append_to_partitioned_text(
                    messages_to_send,
                    f"\n\nTotal of {len(unlinkable_users[reason])} {reason}",
                    MAX_MESSAGE_LENGTH,
                    text_to_end_nonlast_parts,
                    text_to_start_nonfirst_parts,
                )

        messages_count = len(messages_to_send)
        if messages_count == 0:
            logger.info("No unlinkable users found")
        elif messages_count == 1:
            logger.info(f"Sending the list of GitHub users unlinkable to Slack users to Slack channel {slack_channel}")
        else:
            logger.info(
                (
                    f"Sending the list of GitHub users unlinkable to Slack users to Slack channel {slack_channel} "
                    f"in {messages_count} messages "
                    f"(to avoide Slack message size limit of {MAX_MESSAGE_LENGTH} chars)"
                )
            )

        for message in messages_to_send:
            logger.debug(f"Sending message to Slack {slack_channel}: {message}")
            send_message_on_slack(
                recipient=slack_channel, text="GitHub users unlinkable to Slack users", markdown_text=message
            )

            # Sleep to improve the chances of messages being sent in order
            sleep(2)

        logger.info("Finished sending messages to Slack channel")

    except Exception as e:
        raise_with_context(e, RuntimeError, "Failed to publish list of unlinkable users")


if __name__ == "__main__":
    try:
        publish_list_of_unlinkable_users()
    except Exception as e:
        exit_on_error_and_write_summary(f"Couldn't publish list of unlinkable users due to an error: {e}")
