import json
from dataclasses import dataclass
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured


class PaymentGatewayError(Exception):
    def __init__(self, message, payload=None):
        super().__init__(message)
        self.payload = payload or {}


@dataclass
class PaymentRequestResult:
    authority: str
    payment_url: str
    payload: dict


class ZarinpalGateway:
    def __init__(self):
        self.merchant_id = settings.ZARINPAL_SANDBOX_MERCHANT_ID if settings.ZARINPAL_SANDBOX else settings.ZARINPAL_MERCHANT_ID
        if not self.merchant_id:
            raise ImproperlyConfigured('شناسه پذیرنده زرین‌پال تنظیم نشده است.')
        host = 'sandbox.zarinpal.com' if settings.ZARINPAL_SANDBOX else 'api.zarinpal.com'
        pay_host = 'sandbox.zarinpal.com' if settings.ZARINPAL_SANDBOX else 'www.zarinpal.com'
        self.request_url = f'https://{host}/pg/v4/payment/request.json'
        self.verify_url = f'https://{host}/pg/v4/payment/verify.json'
        self.start_url = f'https://{pay_host}/pg/StartPay/'

    @staticmethod
    def _amount_in_rials(amount_toman):
        return int(amount_toman) * 10

    def request_payment(self, *, order, callback_url):
        payload = {
            'merchant_id': self.merchant_id,
            'amount': self._amount_in_rials(order.amount),
            'callback_url': callback_url,
            'description': f'خرید دوره {order.course.name} - سفارش {order.pk}',
            'metadata': {'mobile': order.user.phone, 'email': order.user.email or ''},
        }
        response = self._post(self.request_url, payload)
        data = response.get('data') or {}
        if data.get('code') != 100 or not data.get('authority'):
            raise PaymentGatewayError(self._error_message(response), response)
        authority = data['authority']
        return PaymentRequestResult(authority, f'{self.start_url}{authority}', response)

    def verify_payment(self, *, order, authority):
        response = self._post(self.verify_url, {
            'merchant_id': self.merchant_id,
            'amount': self._amount_in_rials(order.amount),
            'authority': authority,
        })
        data = response.get('data') or {}
        if data.get('code') not in (100, 101):
            raise PaymentGatewayError(self._error_message(response), response)
        return data, response

    @staticmethod
    def _error_message(payload):
        errors = payload.get('errors') or {}
        return errors.get('message') or 'پاسخ نامعتبر از درگاه پرداخت دریافت شد.'

    @staticmethod
    def _post(url, payload):
        request = Request(url, data=json.dumps(payload).encode('utf-8'), headers={'Content-Type': 'application/json', 'Accept': 'application/json'}, method='POST')
        try:
            with urlopen(request, timeout=15) as response:
                return json.loads(response.read().decode('utf-8'))
        except HTTPError as exc:
            try:
                body = json.loads(exc.read().decode('utf-8'))
            except (ValueError, UnicodeDecodeError):
                body = {'errors': {'message': f'خطای HTTP {exc.code} از درگاه'}}
            raise PaymentGatewayError(ZarinpalGateway._error_message(body), body) from exc
        except (URLError, TimeoutError, json.JSONDecodeError) as exc:
            raise PaymentGatewayError('ارتباط با درگاه پرداخت برقرار نشد.', {'exception': str(exc)}) from exc
