import datetime
import os
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 DEFAULT_PR_UPDATE_NOTIFICATION_THRESHOLD_HOURS, GITHUB_TOKEN_ENV_VAR
from github.env import exit_on_error_and_write_summary, write_github_summary
from github.github_apis import get_all_repositories, get_pull_requests
from github.slack_user_link import get_slack_user_by_github_user
from slack_wrapper.constants import MAX_MESSAGE_LENGTH
from slack_wrapper.message import send_message_on_slack


def process_pr(
    all_prs: dict, reviewers_assigned_prs_map: dict, pr: dict, update_threshold_hours: int, pr_created_by: str
) -> tuple[dict, dict]:
    """
    Process a single pull request to check if it meets the criteria for reminders.

    This function checks if the pull request was created by a specific user and if it has not been updated
    for more than a specified number of hours. If both conditions are met, it adds the pull request
    to the list of all pull requests and categorizes it by the reviewers assigned to it.

    :param all_prs: A dictionary to hold all pull requests that meet the criteria.
    :param reviewers_assigned_prs_map: A dictionary to hold reviewers and the pull requests they need to review.
    :param pr: The pull request data to process.
    :param update_threshold_hours: The number of hours after which a pull request is considered stale.
    :param pr_created_by: The GitHub username of the user who created the pull requests to be checked.
    :return: A tuple containing the updated dictionaries of all pull requests and reviewers.
    """
    try:
        repo_full_name = pr["base"]["repo"]["full_name"]
        logger.debug(f"Processing PR in repo {repo_full_name}: {pr}")
        if pr["user"]["login"] == pr_created_by:
            update_time = pr.get("updated_at") or pr.get("created_at")
            hours_since_update = (
                datetime.datetime.now(datetime.timezone.utc)
                - datetime.datetime.fromisoformat(update_time[:-1] + "+00:00")
            ).total_seconds() / 3600
            if hours_since_update > update_threshold_hours:
                logger.info(
                    f"PR {pr['number']} in {repo_full_name} has not been updated for more than "
                    f"{update_threshold_hours} hours. Getting reviewers to send reminders to."
                )
                all_prs[f"{repo_full_name}/{pr['number']}"] = pr
                reviewers_in_pr = pr.get("requested_reviewers", [])
                logger.debug(f"Reviewers in PR {pr['number']}: {reviewers_in_pr}")
                for reviewer in reviewers_in_pr:
                    if reviewer["login"] not in reviewers_assigned_prs_map:
                        reviewers_assigned_prs_map[reviewer["login"]] = []
                    reviewers_assigned_prs_map[reviewer["login"]].append(f"{repo_full_name}/{pr['number']}")
        return all_prs, reviewers_assigned_prs_map
    except Exception as e:
        raise_with_context(e)
        return {}, {}


def get_all_open_pull_requests(
    update_threshold_hours: int,
    pr_created_by: str = "sayuser",
    token_env_var: str = "GLOBAL_CICD_DASHBOARDS_GIT_TOKEN",
) -> tuple[dict, dict]:
    """
    Get all open pull requests across all repositories in the organization.
    This function retrieves all open pull requests that are made by the provided user (default is 'sayuser')
    and have not been updated for more than `update_threshold_hours` hours.

    It categorizes them by the reviewers assigned to each pull request and sends Slack messages
    to remind those reviewers about the pending reviews.

    :param update_threshold_hours: The number of hours after which a pull request is considered stale.
    :param pr_created_by: The GitHub username of the user who created the pull requests to be checked.
    :param token_env_var: The environment variable name that contains the GitHub API token.
    :return: A tuple containing two dictionaries:
        - all_prs: A dictionary with pull request details, where keys are 'repo_name/PR_number'.
        - reviewers_assigned_prs_map: A dict with reviewers usernames as keys and lists of PRs they need to review.
    """
    try:
        logger.info(f"Setting the {GITHUB_TOKEN_ENV_VAR} variable to get token from env var {token_env_var}.")
        os.environ[GITHUB_TOKEN_ENV_VAR] = token_env_var

        logger.info("Starting to fetch all open pull requests across all repositories.")
        all_repos = get_all_repositories()

        # Initialize dictionaries to hold pull requests and reviewers
        all_prs = {}
        reviewers_assigned_prs_map = {}
        failed_repos = []

        logger.info(f"Found {len(all_repos)} repositories in the organization.")
        for repo in all_repos:
            try:
                logger.info(f"Getting pull requests for repository: {repo['full_name']}")
                pull_requests = get_pull_requests(repo=repo["full_name"], state="open")
                for pr in pull_requests:
                    all_prs, reviewers_assigned_prs_map = process_pr(
                        all_prs, reviewers_assigned_prs_map, pr, update_threshold_hours, pr_created_by
                    )
            except Exception as e:
                logger.error(f"Skipping repository {repo['full_name']}; Failed to get pull requests: {e}")
                failed_repos.append({repo["full_name"]: str(e)})
        logger.info(f"Reviewers to notify on open pull requests: {reviewers_assigned_prs_map}")
        logger.debug(f"All open pull requests: {all_prs}")
        if failed_repos:
            write_github_summary(f"Failed to get pull requests for repos: {failed_repos}")
        return all_prs, reviewers_assigned_prs_map
    except Exception as e:
        raise_with_context(e)
        return {}, {}


def compose_reminder_messages_for_reviewer(
    reviewer_name_to_greet: str,
    prs_to_be_reviewed: list,
    all_prs: dict,
    text_to_end_nonlast_parts_with: str,
    text_to_start_nonfirst_parts_with: str,
) -> list:
    """
    Compose reminder messages for a specific reviewer about open pull requests that need their attention.

    :param reviewer_name_to_greet: The name of the reviewer.
    :param prs_to_be_reviewed: A list of pull requests that need to be reviewed by the reviewer.
    :param all_prs: A dictionary containing all pull requests with their details.
    :param text_to_end_nonlast_parts_with: Text to append at the end of non-last parts of the message.
    :param text_to_start_nonfirst_parts_with: Text to prepend at the start of non-first parts of the message.
    :return: A list of messages to be sent to the reviewer.
    """
    try:
        messages_to_send = []
        append_to_partitioned_text(
            messages_to_send,
            (
                f"Hello {reviewer_name_to_greet},\n"
                "There are PRs waiting for your review. "
                "Please review 👀, approve ✅, and merge 🔀 them at your earliest convenience.\n\n"
                "Active PRs are:\n"
            ),
            MAX_MESSAGE_LENGTH,
            text_to_end_nonlast_parts_with,
            text_to_start_nonfirst_parts_with,
        )
        for pr in sorted(set(prs_to_be_reviewed)):
            append_to_partitioned_text(
                messages_to_send,
                f"🛠️ <{all_prs[pr]['html_url']}|PR #{all_prs[pr]['number']}>: 📝 {all_prs[pr]['title']}\n",
                MAX_MESSAGE_LENGTH,
                text_to_end_nonlast_parts_with,
                text_to_start_nonfirst_parts_with,
            )

        return messages_to_send
    except Exception as e:
        raise_with_context(
            e, RuntimeError, f"Failed to compose reminder messages for reviewer {reviewer_name_to_greet}"
        )
        return []


def main():
    try:
        update_threshold_hours = int(
            os.getenv("UPDATE_THRESHOLD_HOURS", DEFAULT_PR_UPDATE_NOTIFICATION_THRESHOLD_HOURS)
        )
        pr_created_by = os.getenv("PR_CREATED_BY", "sayuser")
        token_env_var = os.getenv("TOKEN_ENV_VAR", "GLOBAL_CICD_DASHBOARDS_GIT_TOKEN")

        all_prs, reviewers_assigned_prs_map = get_all_open_pull_requests(
            update_threshold_hours, pr_created_by, token_env_var
        )

        text_to_end_nonlast_parts_with = f"Message limited to {MAX_MESSAGE_LENGTH} characters. To be continued..."
        text_to_start_nonfirst_parts_with = "Continuing the list of PRs..."
        for reviewer in reviewers_assigned_prs_map:
            logger.debug(f"Looking up Slack user for reviewer {reviewer}")
            slack_user = get_slack_user_by_github_user(reviewer)
            if slack_user:
                messages_to_send = compose_reminder_messages_for_reviewer(
                    f"<@{slack_user['id']}>",
                    reviewers_assigned_prs_map[reviewer],
                    all_prs,
                    text_to_end_nonlast_parts_with,
                    text_to_start_nonfirst_parts_with,
                )

                for message in messages_to_send:
                    logger.debug(f"Sending reminder via Slack to {reviewer} about PRs: {message}")
                    send_message_on_slack(
                        recipient=slack_user["id"], text="Reminder on open PRs", markdown_text=message
                    )

                    # Sleep to improve the chances of messages being sent in order
                    sleep(2)
            else:
                logger.warning(
                    f"Cannot send Slack direct message to GitHub user {reviewer} (user cannot be found on Slack)."
                )
    except Exception as e:
        exit_on_error_and_write_summary(f"Failed to notify users on open pull requests: {e}")


if __name__ == "__main__":
    main()
