import logging

from celery.utils.log import get_task_logger
from django.db.models import Q

from apps.google import constants
from apps.google.client import (
    GoogleCalendarAPIClient,
    GoogleCalendarGenericHTTPError,
    GoogleCalendarRefreshError,
    GoogleCalendarUnauthorizedHTTPError,
)
from apps.google.models import GoogleOAuth2User
from apps.schedules.models import OnCallSchedule, ShiftSwapRequest
from common.custom_celery_tasks import shared_dedicated_queue_retry_task

logger = get_task_logger(__name__)
logger.setLevel(logging.DEBUG)


@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True)
def sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk: int) -> None:
    google_oauth2_user = GoogleOAuth2User.objects.get(pk=google_oauth2_user_pk)
    google_api_client = GoogleCalendarAPIClient(google_oauth2_user.access_token, google_oauth2_user.refresh_token)

    user = google_oauth2_user.user
    user_id = user.pk

    logger.info(f"Syncing out of office Google Calendar events for user {user_id}")

    users_schedules = OnCallSchedule.objects.related_to_user(user)
    user_google_calendar_settings = user.google_calendar_settings
    oncall_schedules_to_consider_for_shift_swaps = user_google_calendar_settings[
        "oncall_schedules_to_consider_for_shift_swaps"
    ]

    if oncall_schedules_to_consider_for_shift_swaps:
        users_schedules = users_schedules.filter(public_primary_key__in=oncall_schedules_to_consider_for_shift_swaps)

    try:
        out_of_office_events = google_api_client.fetch_out_of_office_events()
    except GoogleCalendarUnauthorizedHTTPError:
        # this means the user's current token is missing the required scopes, don't delete their token for now
        # we'll notify them via the plugin UI and ask them to reauth and grant us the missing scope
        logger.warning(
            f"Failed to fetch out of office events for user {user_id} due to missing required scopes. "
            "Safe to skip for now"
        )
        return
    except GoogleCalendarRefreshError:
        # in this scenarios there's really not much we can do with the refresh/access token that we
        # have available. The user will need to re-connect with Google so lets delete their persisted token

        logger.exception(
            f"Failed to fetch out of office events for user {user_id} due to an invalid access and/or refresh token"
        )
        user.reset_google_oauth2_settings()
        return
    except GoogleCalendarGenericHTTPError:
        logger.exception(f"Failed to fetch out of office events for user {user_id} due to a generic HTTP error")
        return

    for out_of_office_event in out_of_office_events:
        raw_event = out_of_office_event.raw_event

        event_title = raw_event["summary"]
        event_id = raw_event["id"]
        start_time_utc = out_of_office_event.start_time_utc
        end_time_utc = out_of_office_event.end_time_utc

        logger.info(
            f"Processing out of office event {event_id} starting at {start_time_utc} and ending at "
            f"{end_time_utc} for user {user_id}"
        )

        if constants.EVENT_SUMMARY_IGNORE_KEYWORD in event_title.lower():
            logger.info(
                f"Skipping out of office event {event_id} because it contains the ignore keyword "
                f"'{constants.EVENT_SUMMARY_IGNORE_KEYWORD}'"
            )
            continue

        for schedule in users_schedules:
            _, current_shifts, upcoming_shifts = schedule.shifts_for_user(
                user,
                start_time_utc,
                datetime_end=end_time_utc,
            )

            if current_shifts or upcoming_shifts:
                logger.info(
                    f"Found {len(current_shifts)} current shift(s) and {len(upcoming_shifts)} upcoming shift(s) "
                    f"for user {user_id} during the out of office event {event_id}"
                )

                # also consider deleted shift swap requests.. this can be useful in the event
                # that we autogenerated a shift swap request for the user but the user decided to delete it
                # in this case, we shouldn't recreate a new one
                shift_swap_request_exists = ShiftSwapRequest.objects_with_deleted.filter(
                    beneficiary=user,
                    schedule=schedule,
                    swap_start=start_time_utc,
                    swap_end=end_time_utc,
                ).exists()

                if not shift_swap_request_exists:
                    logger.info(
                        f"Creating shift swap request for user {user_id} schedule {schedule.pk} "
                        f"due to the out of office event {event_id}"
                    )

                    ssr = ShiftSwapRequest.objects.create(
                        beneficiary=user,
                        schedule=schedule,
                        swap_start=start_time_utc,
                        swap_end=end_time_utc,
                        description=f"{user.name or user.email} will be out of office during this time according to Google Calendar",
                    )

                    logger.info(f"Created shift swap request {ssr.pk}")
                else:
                    logger.info(f"Shift swap request already exists for user {user_id} schedule {schedule.pk}")
            else:
                logger.info(
                    f"No current or upcoming shifts found for user {user_id} during the out of office event {event_id}"
                )


@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True)
def sync_out_of_office_calendar_events_for_all_users() -> None:
    # some existing tokens may not have all the required scopes, lets skip these
    tokens_containing_required_scopes = GoogleOAuth2User.objects.filter(
        *[Q(oauth_scope__contains=scope) for scope in constants.REQUIRED_OAUTH_SCOPES],
        user__organization__deleted_at__isnull=True,
    )

    logger.info(
        f"Google OAuth2 tokens with the required scopes - "
        f"{tokens_containing_required_scopes.count()}/{GoogleOAuth2User.objects.count()}"
    )

    for google_oauth2_user in tokens_containing_required_scopes:
        sync_out_of_office_calendar_events_for_user.apply_async(args=(google_oauth2_user.pk,))
