"""
Python API for persona_integration.
"""
import datetime
import json
import logging

import requests
from django.conf import settings

from persona_integration.exceptions import InvalidStatusTransition, PersonaCannotRegisterAttempt
from persona_integration.models import UserPersonaAccount, VerificationAttempt
from persona_integration.statuses import VerificationAttemptStatus

log = logging.getLogger(__name__)


def create_inquiry(user, verified_name, idempotency_key=None):
    """
    Send a request to Persona to create an Inquiry with the user's information.

    Arguments:
        * user (User): the user (usually a learner) who will be performing the inquiry
        * verified_name (string): the verified name of the user
        * idempotency_key (string): the key used to make sure that inquiries are idempotent,
          i.e. ensure that Persona ignores requests with the same idempotency key so repeat
          requests (i.e. retries) do not create a new inquiry for each indentical requests
          meant to represent a single inquiry. It is optional and defaults to None.

    Returns:
        * status (string): the response code from Persona

    Note that this api function may also raise the PersonaCannotRegisterAttempt exception
    if the request to Persona fails.
    """
    expiration_days = settings.VERIFY_STUDENT['DAYS_GOOD_FOR']
    inquiry_template_id = settings.VERIFY_STUDENT['PERSONA']['INQUIRY_TEMPLATE_ID']
    api_key = settings.VERIFY_STUDENT['PERSONA']['API_KEY']
    retry_url = f'{settings.ACCOUNT_MICROFRONTEND_URL}/'

    email = user.email
    user_persona_account, _ = UserPersonaAccount.objects.get_or_create(user=user)
    account_reference_id = user_persona_account.external_user_id

    headers = {
        'accept': 'application/json',
        'Persona-Version': '2023-01-05',
        'Key-Inflection': 'snake',  # Snake case in Python, then change to camel case in frontend.
        'authorization': f'Bearer {api_key}',
    }

    # The idempotency key allows us to retry this request in the case of a network failure. It is optional.
    if idempotency_key:
        headers['Idempotency-Key'] = idempotency_key

    payload = {
        'data': {
            'attributes': {
                'inquiry_template_id': inquiry_template_id,
                'fields': {
                    'name_first': verified_name,
                    'email': email,
                    'expiration_days': expiration_days,
                    'retry_url': retry_url,
                },
            },
        },
        'meta': {
            'auto-create-account-reference-id': str(account_reference_id),
        }
    }

    api_root = settings.VERIFY_STUDENT['PERSONA']['API_ROOT']
    url = f'{api_root}/inquiries'

    # To keep things simple, this application does not handle retrying this request if it fails.
    # The client may handle retrying this request via the VerificationAttemptView if it fails.
    try:
        response = requests.post(url, headers=headers, json=payload, timeout=10)

        # If Persona returns an non-success status code, that means we coded something wrong in this backend.
        # In which case, we should log the response and raise an exception.
        if response.status_code >= 300:
            log.error(
                'Persona failed to create an inquiry with the following response: %(response_msg)s '
                'with status code: %(http_status)s',
                {
                    'http_status': response.status_code,
                    'response_msg': json.dumps(response.json()),
                }
            )
            raise PersonaCannotRegisterAttempt

        inquiry_id = response.json().get('data', {}).get('id')

        # If the requests is successful, return the 2XX status code.
        log.info(
            'Request to Persona to create an inquiry (IDV attempt) with inquiry_id=%(inquiry_id)s '
            'for user with user_id=%(user_id)s was successful with response status=%(response_status)s)',
            {
                # NOTE: The response.data object as a whole contains PII, so only ever send back the id
                'inquiry_id': inquiry_id,
                'user_id': user.id,
                'response_status': response.status_code,
            }
        )
        return response.status_code, inquiry_id

    # If the request to Persona doesn't return anything and fails for another reason, something is likely wrong
    # on Persona's end. In this case, we should log a separate error.This captures several errors that could be
    # raised by the `requests` package: https://3.python-requests.org/user/quickstart/#errors-and-exceptions
    except Exception as error_message:
        log.error(
            'Failed to connect to Persona with error message: %(error_message)s',
            {'error_message': str(error_message)}
        )
        raise PersonaCannotRegisterAttempt from error_message


def get_expiration_date(date):
    """
    Return the expiration date of a verification attempt given a date.

    Arguments:
        * date (datetime): the date from which an expiration date is calculated

    Returns:
        * expiration date (datetime)
    """
    return date + datetime.timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])


def get_user_by_external_id(external_user_id):
    """
    Get a user associated with an external_user_id.

    Arguments:
        * external_user_id (UUID): the external user ID that is sent to Persona and uniquely identifies the user

    Returns:
        * user (User) if one exists; else, None
    """
    try:
        return UserPersonaAccount.objects.get(external_user_id=external_user_id).user
    except UserPersonaAccount.DoesNotExist:
        return None


def create_or_update_verification_attempt(user, name, status, inquiry_id, event_created_at):
    """
    Create or update a Verification Attempt.

    Create a VerificationAttempt with the values user, name, status, and event_created_at if one associated with the
    inquiry_id does not already exist. Otherwise, update the VerificationAttempt associated with the inquiry_id.

    Arguments:
        * user (User): the user associated with the inquiry
        * name (str): the name the user is verifying, which we sent to Person when creating the inquiry
        * status (str): the status of the inquiry
        * inquiry_id (str): the ID of the Persona inquiry,
        * event_created_at (datetime): the created_at field of the event

    Returns:
        * None
    """
    if not VerificationAttemptStatus.is_valid_status(status):
        msg = (
            'The requested status {status} is invalid. '.format(
                status=status
            )
        )
        raise InvalidStatusTransition(msg)

    (verification_attempt, created) = VerificationAttempt.objects.get_or_create(
        inquiry_id=inquiry_id,
        defaults={'user': user, 'name': name, 'status': status, 'event_created_at': event_created_at}
    )

    # If the VerificationAttempt already exists, do not apply the update if the event is a duplicate
    # or if the associated event was received out of order. Otherwise, perform the update.
    if not created:
        if verification_attempt.status == status or event_created_at < verification_attempt.event_created_at:
            return

        verification_attempt.event_created_at = event_created_at

        verification_attempt.update_status(status)

    # Calculate the expiration date using the created_at timestamp, which is the time at which the approved event
    # was created. This ties the expiration to the time the approval event is created and not to the time that the
    # event is received.
    if status == VerificationAttemptStatus.approved:
        expiration_date = get_expiration_date(event_created_at)
        verification_attempt.expiration_date = expiration_date

    verification_attempt.save()
