from __future__ import unicode_literals

import hashlib
import json
import warnings
from typing import Literal, List, Any, Dict, Optional

from django.core.cache import cache
import requests
from django.http import HttpRequest
from requests.auth import HTTPBasicAuth

from django_paypal import settings as django_paypal_settings
from django_paypal.exceptions import PaypalAuthFailure, PaypalAPIError, PaypalWebhookVerificationError, PaypalOrderAlreadyCapturedError
from django_paypal.models import (
    PaypalOrder,
    PaypalAPIPostData,
    PaypalAPIResponse,
    PaypalWebhook,
)
from django_paypal.api_types import (
    OAuthResponse,
    PaymentSource,
    ApplicationContext,
    ExperienceContext,
    PurchaseUnit,
    APIAuthCredentials,
    OrderCreatedAPIResponse,
    OrderCaptureAPIResponse,
    OrderDetailAPIResponse,
    Intent,
)
from django_paypal.utils import build_paypal_full_uri
from django_paypal.signals import order_captured, order_created


class PaypalWrapper(object):
    interface_version = 'django_paypal_v{}'.format(django_paypal_settings.DJANGO_PAYPAL_VERSION)

    api_url = django_paypal_settings.PAYPAL_API_URL
    sandbox_url = django_paypal_settings.PAYPAL_SANDBOX_API_URL
    orders_api_endpoint = django_paypal_settings.PAYPAL_ORDERS_API_ENDPOINT
    auth_url = django_paypal_settings.PAYPAL_AUTH_URL
    auth_cache_timeout = django_paypal_settings.PAYPAL_AUTH_CACHE_TIMEOUT
    auth_cache_key = django_paypal_settings.PAYPAL_AUTH_CACHE_KEY

    auth: Optional[APIAuthCredentials] = None

    def __init__(self, auth: APIAuthCredentials, sandbox=None):
        super(PaypalWrapper, self).__init__()
        self.auth = auth
        if sandbox or django_paypal_settings.PAYPAL_SANDBOX:
            self.api_url = self.sandbox_url

    def create_order(
        self,
        intent: Intent,
        purchase_units: List[PurchaseUnit],
        payment_source: PaymentSource,
        application_context: Optional[ApplicationContext] = None,
        success_url=django_paypal_settings.PAYPAL_SUCCESS_URL,
        cancellation_url=django_paypal_settings.PAYPAL_CANCELLATION_URL,
    ) -> OrderCreatedAPIResponse:
        if payment_source.paypal and (success_url or cancellation_url):
            if not payment_source.paypal.experience_context:
                payment_source.paypal.experience_context = ExperienceContext(
                    return_url=build_paypal_full_uri(success_url),
                    cancel_url=build_paypal_full_uri(cancellation_url),
                )
            else:
                if not payment_source.paypal.experience_context.return_url:
                    payment_source.paypal.experience_context.return_url = build_paypal_full_uri(success_url)
                if not payment_source.paypal.experience_context.cancel_url:
                    payment_source.paypal.experience_context.cancel_url = build_paypal_full_uri(cancellation_url)

        order_data = {'intent': intent, 'purchase_units': [purchase_unit.to_dict() for purchase_unit in purchase_units]}
        if payment_source:
            order_data.update({'payment_source': payment_source.to_dict()})
        if application_context:
            order_data.update({'application_context': application_context.to_dict()})

        url = '{0}{1}'.format(self.api_url, self.orders_api_endpoint)
        order_response_dict = self.call_api(url=url, data=order_data, method='POST')
        order_response = OrderCreatedAPIResponse.from_dict(order_response_dict)
        new_order = PaypalOrder.objects.create(order_id=order_response.id, status=order_response.status)
        post_obj = PaypalAPIPostData.objects.create(order=new_order, url=url, post_data=order_data)
        PaypalAPIResponse.objects.create(order=new_order, url=url, response_data=order_response_dict, post=post_obj)
        order_created.send(sender=self.__class__, order=new_order, response=order_response_dict)
        return order_response

    def capture_order(self, order_id: str) -> OrderCaptureAPIResponse:
        order = PaypalOrder.objects.get(order_id=order_id)
        url = f'{self.api_url}{self.orders_api_endpoint}/{order.order_id}/capture'

        try:
            order_capture_response = self.call_api(url=url, method='POST')
        except PaypalAPIError as e:
            if e.response and e.response.status_code == 422:
                order_capture_response = e.response.json()
                if order_capture_response.get('name') == 'UNPROCESSABLE_ENTITY':
                    if order_capture_response['details'][0]['issue'] == 'ORDER_ALREADY_CAPTURED':
                        raise PaypalOrderAlreadyCapturedError(str(e), response=e.response)
            raise e

        order_capture = OrderCaptureAPIResponse.from_dict(order_capture_response)
        captures_id_list = []

        if order_capture.purchase_units:
            for purchase_unit in order_capture.purchase_units:
                if purchase_unit.payments and purchase_unit.payments.captures:
                    for capture in purchase_unit.payments.captures:
                        if capture.id:
                            if capture.id in order.capture_id:
                                raise PaypalOrderAlreadyCapturedError('Order already captured')
                            captures_id_list.append(capture.id)

        order.status = order_capture.status or ''
        order.capture_id = captures_id_list
        order.save(update_fields=['status', 'capture_id'])
        post_obj = PaypalAPIPostData.objects.create(order=order, url=url, post_data={})
        PaypalAPIResponse.objects.create(order=order, url=url, response_data=order_capture_response, post=post_obj)
        order_captured.send(sender=self.__class__, order=order, response=order_capture_response)
        return order_capture

    def get_order_details(self, order_id: str) -> OrderDetailAPIResponse:
        order = PaypalOrder.objects.get(order_id=order_id)
        url = f'{self.api_url}{self.orders_api_endpoint}/{order.order_id}'
        order_details_response = self.call_api(url=url, method='GET')
        return OrderDetailAPIResponse.from_dict(order_details_response)

    def call_api(self, url: str, method: Literal['GET', 'POST', 'PATCH', 'DELETE'], data=None) -> Dict[str, Any]:
        headers = {'Authorization': f'Bearer {self._get_access_token()}', 'Content-Type': 'application/json'}

        try:
            if method == 'GET':
                response = requests.get(url, headers=headers)
            elif method == 'POST':
                response = requests.post(url, headers=headers, json=data)
            elif method == 'PATCH':
                response = requests.patch(url, headers=headers, json=data)
            elif method == 'DELETE':
                response = requests.delete(url, headers=headers)
                response.raise_for_status()
                return {'deleted': True}
            else:
                raise ValueError('Invalid method')
            response.raise_for_status()
            response_json = response.json()
            return response_json
        except requests.HTTPError as e:
            raise PaypalAPIError(str(e), response=e.response)

    def setup_webhooks(self, webhook_listener: str) -> PaypalWebhook:
        if PaypalWebhook.objects.filter(url=webhook_listener, auth_hash=self.api_auth_hash).exists():
            raise ValueError(f'Webhook listener ({webhook_listener}) already exists')
        from django_paypal.webhooks import WebhookEvents

        data = {'url': webhook_listener, 'event_types': [{'name': event} for event in WebhookEvents.ORDERS]}
        webhook_api = '{0}{1}'.format(self.api_url, '/v1/notifications/webhooks')
        try:
            response_dict = self.call_api(url=webhook_api, method='POST', data=data)
            return PaypalWebhook.objects.create(
                webhook_id=response_dict['id'],
                auth_hash=self.api_auth_hash,
                url=response_dict['url'],
                event_types=response_dict['event_types'],
            )
        except PaypalAPIError as e:
            if e.response:
                response_json = e.response.json()
                if response_json.get('name') == 'WEBHOOK_URL_ALREADY_EXISTS':
                    webhook_list = self.call_api(url=webhook_api, method='GET')
                    for webhook in webhook_list.get('webhooks', []):
                        if webhook['url'] == webhook_listener:
                            return PaypalWebhook.objects.create(
                                webhook_id=webhook['id'], url=webhook['url'], event_types=webhook['event_types'], auth_hash=self.api_auth_hash
                            )
            raise e

    def patch_webhook(self, webhook_id: str, patch_data: List[Dict]) -> PaypalWebhook:
        try:
            paypal_webhook = PaypalWebhook.objects.get(webhook_id=webhook_id)
        except PaypalWebhook.DoesNotExist as e:
            warnings.warn(f'Webhook with id {webhook_id} does not exist in the database. Set up webhooks by calling "setup_webhooks"')
            raise e
        url = '{0}{1}'.format(self.api_url, f'/v1/notifications/webhooks/{paypal_webhook.webhook_id}')
        webhook_response_dict = self.call_api(url=url, method='PATCH', data=patch_data)
        paypal_webhook.url = webhook_response_dict['url']
        paypal_webhook.event_types = webhook_response_dict['event_types']
        paypal_webhook.save()
        return paypal_webhook

    def delete_webhook(self, webhook_listener: str) -> bool:
        try:
            paypal_webhook = PaypalWebhook.objects.get(url=webhook_listener, auth_hash=self.api_auth_hash)
        except PaypalWebhook.DoesNotExist as e:
            raise e
        url = '{0}{1}'.format(self.api_url, f'/v1/notifications/webhooks/{paypal_webhook.webhook_id}')
        self.call_api(url=url, method='DELETE')
        paypal_webhook.delete()
        return True

    def verify_webhook_event(self, request: HttpRequest, webhook_id: str) -> bool:
        headers = request.headers
        auth_algo = headers.get('Paypal-Auth-Algo')
        cert_url = headers.get('Paypal-Cert-Url')
        transmission_id = headers.get('Paypal-Transmission-Id')
        transmission_time = headers.get('Paypal-Transmission-Time')
        transmission_sig = headers.get('Paypal-Transmission-Sig')
        request_data = json.loads(request.body.decode('utf-8'))
        verification_payload = {
            'auth_algo': auth_algo,
            'cert_url': cert_url,
            'transmission_id': transmission_id,
            'transmission_time': transmission_time,
            'transmission_sig': transmission_sig,
            'webhook_id': webhook_id,
            'webhook_event': request_data,
        }
        url = f'{self.api_url}/v1/notifications/verify-webhook-signature'
        res = self.call_api(url=url, method='POST', data=verification_payload)
        if res.get('verification_status') == 'SUCCESS':
            return True
        raise PaypalWebhookVerificationError('Webhook verification failed', res)

    def verify_api_keys(self) -> bool:
        self._get_access_token()
        return True

    def _get_access_token(self) -> str:
        if not self.auth_cache_timeout:
            auth_response = self._authorize_client()
            return auth_response.access_token
        else:
            cache_key = self.auth_cache_key.format(auth_hash=self.api_auth_hash)
            access_token = cache.get(cache_key)
            if not access_token:
                auth_response = self._authorize_client()
                access_token = auth_response.access_token
                cache.set(cache_key, access_token, self.auth_cache_timeout)
            return access_token

    def _authorize_client(self) -> OAuthResponse:
        if not self.auth:
            raise ValueError('Auth credentials not set')
        
        # preparing request
        url = f'{self.api_url}{self.auth_url}'

        api_auth = HTTPBasicAuth(self.auth.client_id, self.auth.client_secret)
        headers = {'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded'}
        data = {'grant_type': 'client_credentials'}

        try:
            response = requests.post(url, headers=headers, data=data, auth=api_auth)
            response.raise_for_status()  # Raise an exception for HTTP errors
            response_json = response.json()
            return OAuthResponse(**response_json)
        except requests.HTTPError as e:
            raise PaypalAuthFailure(str(e), response=e.response)

    @property
    def api_auth_hash(self) -> str:
        if not self.auth:
            raise ValueError('Auth credentials not set')
        return hashlib.md5(f'{self.auth.client_id}{self.auth.client_secret}'.encode()).hexdigest()
