import os
import re
from uuid import UUID

from oscar.core.loading import get_class, get_model

from ocyan.core.fender import config
from ocyan.core.utils import round_to_two_dec
from ocyan.mollie.constants import (
    MOLLIE_LOCALES,
    ORDER_API_METHODS,
    is_supported_payment_method,
    normalize_payment_method,
)
from ocyan.mollie.mixins import MolliePaymentMixin
from ocyan.mollie.provider import (
    MollieConfigurationError,
    MollieResponseError,
    checkout_url,
    format_amount,
    format_component_amount,
)
from ocyan.plugin.oscar.pipeline import (
    PIPELINE_STATUS_CANCELLED,
    PIPELINE_STATUS_PENDING_PAYMENT,
)
from ocyan.plugin.oscar.utils import (
    create_or_record_payment_events,
    create_payment_events,
)
from ocyan.plugin.oscar_partner.partner.utils import tax_from_price_incl_tax
from ocyan.plugin.oscar_shipping.shipping.utils import TAX_RATE as SHIPPING_TAX_RATE

from .models import MolliePaymentAttempt

Source = get_model("payment", "Source")
PaymentEvent = get_model("order", "PaymentEvent")
PaymentEventType = get_model("order", "PaymentEventType")
get_vat_info_for_country = get_class("partner.utils", "get_vat_info_for_country")


class MolliePayment(MolliePaymentMixin):
    def __init__(self):
        # Environment configuration takes precedence and is intentionally read
        # only when a payment operation is requested, never at module import.
        api_key = os.environ.get("MOLLIE_API_KEY") or config.get(
            "payment_mollie", "api_key"
        )
        super(MolliePayment, self).__init__(api_key)

    def create_payment(
        self, order, method, webhook, redirect, description=None, idempotency_key=None
    ):
        method = normalize_payment_method(method)
        if not is_supported_payment_method(method):
            raise MollieConfigurationError(
                "The configured Mollie payment method is no longer supported."
            )
        # New checkouts use Mollie's Payments API. The Orders API remains only
        # for reconciliation and shipment handling of pre-existing ``ord_``
        # references created by older releases.
        attempt = self.payment_attempt(order, method, idempotency_key)
        payment = self.create_mollie_payment(
            order,
            method,
            webhook,
            redirect,
            description,
            idempotency_key=str(attempt.idempotency_key),
        )
        provider_reference = str(payment["id"])
        if attempt.provider_reference != provider_reference:
            attempt.provider_reference = provider_reference
            attempt.save(update_fields=("provider_reference",))

        order.sources.get_or_create(
            order=order,
            source_type=self.get_source(method),
            currency=order.currency,
            reference=provider_reference,
            defaults={"amount_allocated": order.total_incl_tax},
        )
        if order.sources.count() > 1:
            order.sources.exclude(reference=provider_reference).delete()

        create_or_record_payment_events(
            order,
            PIPELINE_STATUS_PENDING_PAYMENT,
            "Mollie payment update",
            provider_reference,
        )

        # A current Payments API create response includes the checkout link.
        # Retain one legacy fetch fallback for older SDK/API responses only.
        try:
            return checkout_url(payment)
        except MollieResponseError:
            resource = self.mollie.payments.get(provider_reference)
            return checkout_url(resource)

    def payment_attempt(self, order, method, idempotency_key=None):
        if idempotency_key is None:
            return MolliePaymentAttempt.objects.create(
                order_number=order.number,
                method=method,
            )
        try:
            key = UUID(str(idempotency_key))
        except (TypeError, ValueError) as exc:
            raise MollieConfigurationError("Invalid Mollie checkout attempt.") from exc
        attempt, _ = MolliePaymentAttempt.objects.get_or_create(
            idempotency_key=key,
            defaults={"order_number": order.number, "method": method},
        )
        if attempt.order_number != order.number or attempt.method != method:
            raise MollieConfigurationError("Invalid Mollie checkout attempt.")
        return attempt

    def create_mollie_payment(
        self, order, method, webhook, redirect, description, idempotency_key
    ):
        description = description if description else "Order %s" % order.number
        data = {
            "amount": format_amount(order.total_incl_tax, order.currency),
            "description": description,
            "redirectUrl": redirect,
            "webhookUrl": webhook,
            "method": method,
            "metadata": {"order_nr": order.number, "order_number": order.number},
        }
        if method in ORDER_API_METHODS:
            data["lines"] = self.order_lines(order)
            data["billingAddress"] = self.get_billing_address(order)
            data["locale"] = MOLLIE_LOCALES.get(
                order.language_code, config.get("django", "language_code")
            )
        return self.mollie.payments.create(data, idempotency_key=idempotency_key)

    def request_refund(self, reference, amount, currency, *, approved_by):
        """Request a Payment API refund after the caller has authorised it.

        This adapter deliberately exposes no public refund route. Dashboard or
        service callers must enforce their own Oscar permission policy before
        passing an approving user; the provider call itself remains explicit
        and uses exact Mollie amount formatting.
        """
        if approved_by is None:
            raise MollieConfigurationError("A refund requires an authorised caller.")
        if not str(reference).startswith("tr_"):
            raise MollieConfigurationError(
                "Only Payments API references can be refunded by this adapter."
            )
        payment = self.mollie.payments.get(reference)
        return payment.refunds.create({"amount": format_amount(amount, currency)})

    def create_mollie_order(self, order, method, webhook, redirect):
        """Legacy-only Orders API creator retained for existing integrations."""
        billing_address = self.get_billing_address(order)
        return self.mollie.orders.create(
            {
                "amount": format_amount(order.total_incl_tax, order.currency),
                "redirectUrl": redirect,
                "webhookUrl": webhook,
                "method": method,
                "orderNumber": order.number,
                "lines": self.order_lines(order),
                "billingAddress": billing_address,
                "locale": MOLLIE_LOCALES.get(
                    order.language_code, config.get("django", "language_code")
                ),
            }
        )

    def order_lines(self, order):
        order_lines = list()
        for line in order.lines.all():
            for line_price in line.prices.all():
                total_amount = line_price.price_incl_tax * line_price.quantity
                vat_rate = get_vat_info_for_country(
                    line.product.vat_rate,
                    order.shipping_address.country.pk,
                    False,
                    basket=getattr(order, "basket", None),
                ).percentage
                order_lines.append(
                    {
                        "name": str(line.title),
                        "quantity": line_price.quantity,
                        "unitPrice": format_amount(
                            line_price.price_incl_tax, order.currency
                        ),
                        "totalAmount": format_amount(total_amount, order.currency),
                        "vatRate": str(vat_rate * 100),
                        "vatAmount": format_component_amount(
                            round_to_two_dec(
                                tax_from_price_incl_tax(total_amount, vat_rate)
                            ),
                            order.currency,
                        ),
                    }
                )

        if order.shipping_incl_tax > 0:
            order_lines.append(self.shipping_line_data(order))

        if order.surcharge_incl_tax > 0:
            for surcharge in self.surcharge_line_data(order):
                order_lines.append(surcharge)
        return order_lines

    def shipping_line_data(self, order):
        vat_rate = get_vat_info_for_country(
            SHIPPING_TAX_RATE,
            order.shipping_address.country.pk,
            True,
            basket=getattr(order, "basket", None),
        ).percentage
        return {
            "name": str(order.shipping_method),
            "quantity": 1,
            "unitPrice": format_amount(order.shipping_incl_tax, order.currency),
            "totalAmount": format_amount(order.shipping_incl_tax, order.currency),
            "vatRate": str(vat_rate * 100),
            "vatAmount": format_component_amount(order.shipping_tax, order.currency),
        }

    def surcharge_line_data(self, order):
        charge_data = list()
        for charge in order.surcharges.all():
            charge_tax = charge.incl_tax - charge.excl_tax
            vat_rate = (charge_tax / charge.excl_tax) * 100
            charge_data.append(
                {
                    "name": str(charge.name),
                    "quantity": 1,
                    "unitPrice": format_amount(charge.incl_tax, order.currency),
                    "totalAmount": format_amount(charge.incl_tax, order.currency),
                    "vatRate": str(round(vat_rate, 2)),
                    "vatAmount": format_component_amount(charge_tax, order.currency),
                }
            )
        return charge_data

    def get_billing_address(self, order):
        if order.billing_address is None:
            addr = order.shipping_address
            email = order.email
        else:
            addr = order.billing_address
            email = order.billing_address.email

        # with some website first name input can be not required
        given_name = addr.first_name if addr.first_name else addr.last_name

        # Klarna requires the givenName to be more than one character, so if the len
        # is equal to 1, just add a '.' as that would likely make sense (H. Pieters)
        if len(given_name) == 1:
            given_name += "."

        return {
            "givenName": given_name,
            "familyName": re.sub(r"[^a-zA-Z0-9\s]", "", addr.last_name),
            "email": email,
            "streetAndNumber": f"{addr.line1} {addr.house_number}",
            "postalCode": addr.postcode,
            "city": addr.city,
            "country": addr.country.pk,
        }

    def create_shipment(self, reference):
        mollie_order = self.mollie.orders.get(reference)
        return mollie_order.shipments.create()

    def cancel_payment_event(self, order, reference):
        if not PaymentEvent.objects.filter(
            reference=reference, event_type__name=PIPELINE_STATUS_CANCELLED
        ).exists():
            create_payment_events(order, PIPELINE_STATUS_CANCELLED, reference)
