"""
Receipt PDF Generator
Generates a branded payment receipt PDF using WeasyPrint + Jinja2.
"""
import ipaddress
import logging
import os
import socket
from datetime import datetime
from typing import Optional
from urllib.parse import urlparse

import jinja2.sandbox
from jinja2 import FileSystemLoader, select_autoescape
from weasyprint import HTML

logger = logging.getLogger(__name__)

_PRIVATE_NETWORKS = [
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("169.254.0.0/16"),
    ipaddress.ip_network("::1/128"),
    ipaddress.ip_network("fc00::/7"),
]


def _safe_url_fetcher(url: str):
    """Custom WeasyPrint URL fetcher that blocks SSRF vectors."""
    from weasyprint.urls import default_url_fetcher as _default_fetcher
    try:
        from src.core.config import settings as _settings
        _is_dev = (getattr(_settings, "APP_ENV", "dev") or "dev").lower() in ("dev", "development", "local")
    except Exception:
        _is_dev = True
    # In dev, skip all URL restrictions so local/HTTP logo URLs render correctly
    if _is_dev:
        return _default_fetcher(url)
    parsed = urlparse(url)
    if parsed.scheme not in ("https",):
        raise ValueError(f"Blocked non-HTTPS URL in PDF template: {parsed.scheme}://")
    try:
        ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
        for network in _PRIVATE_NETWORKS:
            if ip in network:
                raise ValueError(f"Blocked internal IP in PDF template URL: {ip}")
    except socket.gaierror:
        pass
    return _default_fetcher(url)


TEMPLATES_DIR = os.path.normpath(
    os.path.join(os.path.dirname(__file__), "..", "..", "..", "templates")
)


class ReceiptPDFGenerator:
    """Generates a branded payment receipt PDF for a given Transaction ORM object."""

    def __init__(self) -> None:
        self.env = jinja2.sandbox.SandboxedEnvironment(
            loader=FileSystemLoader(TEMPLATES_DIR),
            autoescape=select_autoescape(["html"]),
        )

    def generate(self, transaction, receipt_settings: dict | None = None) -> bytes:
        template = self.env.get_template("receipt_pdf.html")
        context = self._build_context(transaction, receipt_settings or {})
        html_content = template.render(**context)
        return HTML(
            string=html_content,
            base_url=TEMPLATES_DIR,
            url_fetcher=_safe_url_fetcher,
        ).write_pdf()

    # ── Context builder ──────────────────────────────────────────────────────

    def _build_context(self, txn, receipt_settings: dict) -> dict:
        return {
            "txn_literal": txn.txn_literal or txn.txn_id or "",
            "txn_date": self._fmt_date(getattr(txn, "ocurred_at", None) or datetime.utcnow()),
            "txn_amount": self._c(txn.txn_amount),
            "now": datetime.utcnow().strftime("%B %d, %Y"),
            "currency": "$",

            # Merchant
            "merchant_name": getattr(txn.merchant, "name", "") or "",
            **self._get_business_urls(txn),
            "merchant_logo_url": self._get_logo_url(txn),
            "brand_color": self._get_brand_color(txn),

            # Customer / payer
            "payer_name": self._get_payer_name(txn),

            # Payment method
            "payment_method_type": self._get_method_type(txn),
            "card_last_four": self._get_card_last4(txn),

            # Receipt / request details
            "receipt_number": txn.txn_literal or txn.txn_id or "—",
            "payment_frequency": self._fmt_enum(
                self._safe_get(txn, "payment_request.payment_frequency")
            ),

            # Receipt display settings
            "show_customer_name": receipt_settings.get("show_customer_name", True),
            "show_receipt_number": receipt_settings.get("show_receipt_number", True),
            "show_transaction_time": receipt_settings.get("show_transaction_time", True),
            "show_item_description": receipt_settings.get("show_item_description", True),
        }

    # ── Field helpers ────────────────────────────────────────────────────────

    @staticmethod
    def _c(cents) -> str:
        if cents is None:
            return "0.00"
        return f"{cents / 100:.2f}"

    def _fmt_date(self, value) -> str:
        if value is None:
            return "—"
        if isinstance(value, str):
            return value
        try:
            return value.strftime("%m/%d/%Y")
        except Exception:
            return str(value)

    def _safe_get(self, obj, path: str):
        current = obj
        for part in path.split("."):
            if current is None:
                return None
            current = getattr(current, part, None)
        return current

    def _get_payer_name(self, txn) -> str:
        payer = self._safe_get(txn, "payment_method.payer")
        if payer:
            first = getattr(payer, "first_name", "") or ""
            last = getattr(payer, "last_name", "") or ""
            name = f"{first} {last}".strip()
            if name:
                return name
        customer = getattr(txn, "customer", None)
        if customer:
            first = getattr(customer, "first_name", "") or ""
            last = getattr(customer, "last_name", "") or ""
            name = f"{first} {last}".strip()
            if name:
                return name
            biz = getattr(customer, "business_legal_name", "") or ""
            if biz:
                return biz
        return getattr(txn, "billing_name", None) or "—"

    def _get_method_type(self, txn) -> str:
        method = self._safe_get(txn, "payment_method.method")
        if method:
            return str(method).replace("_", " ").title()
        txn_type = getattr(txn, "txn_type", None)
        if txn_type:
            return str(txn_type).replace("_", " ").title()
        return "Card"

    def _get_card_last4(self, txn) -> str:
        card_number = self._safe_get(txn, "payment_method.card_details.card_number")
        if card_number and len(str(card_number)) >= 4:
            return str(card_number)[-4:]
        return "****"

    def _get_invoice_number(self, txn) -> str:
        invoices = getattr(txn, "invoices", None)
        if invoices:
            first = invoices[0] if hasattr(invoices, "__iter__") else None
            if first:
                return getattr(first, "invoice_literal", None) or "—"
        pr = getattr(txn, "payment_request", None)
        if pr:
            return getattr(pr, "payment_request_literal", None) or "—"
        return "—"

    def _get_business_urls(self, txn) -> dict:
        """Read support email, phone, website from branding settings, respecting show toggles."""
        try:
            from src.apps.settings.helper import get_setting_with_default
            merchant = getattr(txn, "merchant", None)
            if not merchant:
                raise ValueError("no merchant")
            mid = merchant.id

            def _get(key): return get_setting_with_default("Business URLs", key, mid) or ""
            def _show(key): return (get_setting_with_default("Business URLs", key, mid) or "true").lower() != "false"

            show_email = _show("show_support_email")
            show_phone = _show("show_support_phone")
            show_website = _show("show_website_url")

            email = _get("support_email") if show_email else ""
            phone = _get("support_phone") if show_phone else ""
            website = _get("website_url") if show_website else ""

            # Fall back to merchant model fields only when the toggle is ON but value is empty
            if show_email and not email:
                email = getattr(merchant, "email", "") or ""
            if show_phone and not phone:
                phone = getattr(merchant, "phone", "") or ""
        except Exception:
            merchant = getattr(txn, "merchant", None)
            email = getattr(merchant, "email", "") or "" if merchant else ""
            phone = getattr(merchant, "phone", "") or "" if merchant else ""
            website = ""
        return {"merchant_email": email, "merchant_phone": phone, "merchant_website": website}

    def _get_logo_url(self, txn) -> str:
        """Read merchant logo URL from merchant_settings and resolve to data URI for WeasyPrint."""
        try:
            from src.apps.settings.helper import get_setting_with_default
            merchant = getattr(txn, "merchant", None)
            if merchant:
                logo = get_setting_with_default("Logo", "logo_url", merchant.id)
                if not logo:
                    logo = self._safe_get(txn, "merchant.brand_logo.full_url") or ""
                if logo:
                    return self._resolve_logo(logo)
        except Exception:
            pass
        return ""

    @staticmethod
    def _resolve_logo(url: str) -> str:
        """Convert a logo URL to a base64 data URI so WeasyPrint reads it from disk."""
        import base64, mimetypes, os
        try:
            from src.core.config import settings as _s
            server_host = str(_s.SERVER_HOST).rstrip("/")
            static_path = str(_s.STATIC_FILES_PATH).rstrip("/")
            uploads_dir = str(_s.UPLOADS_DIR)
            if url.startswith(server_host + static_path):
                rel = url[len(server_host + static_path):]
                file_path = os.path.join(uploads_dir, rel.lstrip("/"))
                if os.path.isfile(file_path):
                    mime, _ = mimetypes.guess_type(file_path)
                    mime = mime or "image/png"
                    with open(file_path, "rb") as f:
                        data = base64.b64encode(f.read()).decode()
                    return f"data:{mime};base64,{data}"
        except Exception:
            pass
        return url

    def _get_brand_color(self, txn) -> str:
        try:
            from src.apps.settings.helper import get_setting_with_default
            merchant = getattr(txn, "merchant", None)
            if merchant:
                color = get_setting_with_default("Colors", "primary_color", merchant.id)
                if color:
                    return color
        except Exception:
            pass
        return "#28B4ED"

    @staticmethod
    def _fmt_enum(value) -> str:
        if not value:
            return "—"
        label_map = {
            "one_time": "Single Payment",
            "single": "Single Payment",
            "split": "Split",
            "recurring": "Recurring",
        }
        key = str(value).lower()
        return label_map.get(key, str(value).replace("_", " ").title())


# Module-level singleton
receipt_pdf_generator = ReceiptPDFGenerator()
