"""
myropay — Python SDK for MyRoPay Checkout Platform

pip install myropay

Usage:
    import myropay
    client = myropay.Client("sk_live_...")

    # Create a session
    session = client.sessions.create(
        amount=5000,
        currency="NGN",
        order_ref="ORD-001",
        description="Premium subscription",
        customer={"email": "buyer@example.com", "name": "Ada Okafor"},
        payment_methods=["card", "wallet", "escrow"],
        escrow=True,
        escrow_condition="Software delivered and tested",
        success_url="https://yoursite.com/success",
    )
    print(session.checkout_url)

    # Verify a webhook
    event = myropay.Webhook.construct_event(
        payload=request.body,
        signature=request.headers["X-MyRoPay-Signature"],
        secret="whsec_...",
    )
    if event.event == "checkout.completed":
        order_ref = event.data["order_ref"]
        amount    = event.data["amount"]
        # fulfil order…
"""

from __future__ import annotations

import hashlib
import hmac
import json
import urllib.request
import urllib.error
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

__version__ = "1.0.0"
API_BASE    = "https://app.myropay.com/checkout/api"


class MyRoPayError(Exception):
    def __init__(self, message: str, status_code: int = 0):
        super().__init__(message)
        self.status_code = status_code


class WebhookSignatureError(MyRoPayError):
    pass


@dataclass
class APIResponse:
    """Thin wrapper that allows both attribute and dict-style access."""
    _data: Dict[str, Any] = field(default_factory=dict)

    def __getattr__(self, name: str) -> Any:
        try:
            val = self._data[name]
            if isinstance(val, dict):
                return APIResponse(_data=val)
            return val
        except KeyError:
            raise AttributeError(f"No attribute '{name}'")

    def __getitem__(self, key: str) -> Any:
        return self._data[key]

    def get(self, key: str, default: Any = None) -> Any:
        return self._data.get(key, default)

    def __repr__(self) -> str:
        return f"APIResponse({self._data!r})"


# ---- HTTP layer ----
class _HttpClient:
    def __init__(self, secret_key: str):
        self._secret_key = secret_key

    def _request(self, method: str, endpoint: str, data: Optional[dict] = None) -> APIResponse:
        url = f"{API_BASE}/{endpoint.lstrip('/')}"
        payload = json.dumps(data).encode() if data else None

        req = urllib.request.Request(
            url,
            data=payload,
            method=method,
            headers={
                "Authorization": f"Bearer {self._secret_key}",
                "Content-Type": "application/json",
                "Accept": "application/json",
                "User-Agent": f"MyRoPay-Python/{__version__}",
            },
        )
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                body = json.loads(resp.read().decode())
                return APIResponse(_data=body)
        except urllib.error.HTTPError as e:
            try:
                err_body = json.loads(e.read().decode())
                msg = err_body.get("error", str(e))
            except Exception:
                msg = str(e)
            raise MyRoPayError(msg, e.code) from e
        except urllib.error.URLError as e:
            raise MyRoPayError(f"Network error: {e.reason}") from e

    def get(self, endpoint: str, params: Optional[dict] = None) -> APIResponse:
        if params:
            from urllib.parse import urlencode
            endpoint = f"{endpoint}?{urlencode(params)}"
        return self._request("GET", endpoint)

    def post(self, endpoint: str, data: Optional[dict] = None) -> APIResponse:
        return self._request("POST", endpoint, data or {})


# ---- Resources ----
class Sessions:
    def __init__(self, http: _HttpClient):
        self._http = http

    def create(
        self,
        amount: float,
        currency: str = "NGN",
        order_ref: Optional[str] = None,
        description: Optional[str] = None,
        customer: Optional[Dict[str, str]] = None,
        payment_methods: Optional[List[str]] = None,
        session_type: str = "standard",
        escrow: bool = False,
        escrow_condition: Optional[str] = None,
        escrow_deadline_days: int = 7,
        milestones: Optional[List[dict]] = None,
        split_parties: Optional[List[dict]] = None,
        instalment_plan: Optional[List[dict]] = None,
        subscription_interval: Optional[str] = None,
        min_amount: Optional[float] = None,
        suggested_amounts: Optional[List[float]] = None,
        success_url: Optional[str] = None,
        cancel_url: Optional[str] = None,
        brand_color: Optional[str] = None,
        brand_logo: Optional[str] = None,
        metadata: Optional[dict] = None,
        custom_fields: Optional[List[dict]] = None,
        expires_in: int = 1440,
    ) -> APIResponse:
        """Create a checkout session. Returns session_id and checkout_url."""
        payload: Dict[str, Any] = {
            "amount": amount,
            "currency": currency,
            "session_type": session_type,
            "escrow": escrow,
            "escrow_deadline_days": escrow_deadline_days,
            "expires_in": expires_in,
        }
        if order_ref:            payload["order_ref"]            = order_ref
        if description:          payload["description"]          = description
        if customer:             payload["customer"]             = customer
        if payment_methods:      payload["payment_methods"]      = payment_methods
        if escrow_condition:     payload["escrow_condition"]     = escrow_condition
        if milestones:           payload["milestones"]           = milestones
        if split_parties:        payload["split_parties"]        = split_parties
        if instalment_plan:      payload["instalment_plan"]      = instalment_plan
        if subscription_interval: payload["subscription_interval"] = subscription_interval
        if min_amount is not None: payload["min_amount"]         = min_amount
        if suggested_amounts:    payload["suggested_amounts"]    = suggested_amounts
        if success_url:          payload["success_url"]          = success_url
        if cancel_url:           payload["cancel_url"]           = cancel_url
        if brand_color:          payload["brand_color"]          = brand_color
        if brand_logo:           payload["brand_logo"]           = brand_logo
        if metadata:             payload["metadata"]             = metadata
        if custom_fields:        payload["custom_fields"]        = custom_fields
        return self._http.post("sessions.php", payload)

    def retrieve(self, session_id: str) -> APIResponse:
        return self._http.get(f"sessions.php?path={session_id}")

    def list(self, page: int = 1, limit: int = 20, status: Optional[str] = None) -> APIResponse:
        params: Dict[str, Any] = {"page": page, "limit": limit}
        if status:
            params["status"] = status
        return self._http.get("sessions.php", params)

    def expire(self, session_id: str) -> APIResponse:
        return self._http.post(f"sessions.php?path={session_id}/expire")


class Refunds:
    def __init__(self, http: _HttpClient):
        self._http = http

    def create(
        self,
        session_id: str,
        amount: float = 0,
        reason: str = "",
    ) -> APIResponse:
        return self._http.post("process_v2.php?action=refund", {
            "session_id": session_id,
            "amount":     amount,
            "reason":     reason,
        })


class Subscriptions:
    def __init__(self, http: _HttpClient):
        self._http = http

    def cancel(self, sub_id: str, immediately: bool = False) -> APIResponse:
        return self._http.post("process_v2.php?action=cancel-subscription", {
            "sub_id":              sub_id,
            "cancel_immediately":  immediately,
        })


class Disputes:
    def __init__(self, http: _HttpClient):
        self._http = http

    def respond(
        self,
        dispute_id: str,
        response: str,
        evidence: str = "",
    ) -> APIResponse:
        return self._http.post("process_v2.php?action=respond-dispute", {
            "dispute_id": dispute_id,
            "response":   response,
            "evidence":   evidence,
        })


class Apps:
    def __init__(self, http: _HttpClient):
        self._http = http

    def stats(self, app_id: int, days: int = 30) -> APIResponse:
        return self._http.get(f"apps.php?action=stats&id={app_id}&days={days}")


# ---- Webhook verification ----
class Webhook:
    @staticmethod
    def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
        expected = hmac.new(
            secret.encode(),
            payload if isinstance(payload, bytes) else payload.encode(),
            hashlib.sha256,
        ).hexdigest()
        return hmac.compare_digest(expected, signature)

    @classmethod
    def construct_event(
        cls,
        payload: bytes,
        signature: str,
        secret: str,
    ) -> APIResponse:
        """
        Parse and verify a webhook.
        Raises WebhookSignatureError if signature doesn't match.

        Args:
            payload:   Raw request body (bytes)
            signature: Value of X-MyRoPay-Signature header
            secret:    Your app's webhook_secret (whsec_...)
        """
        if not cls.verify_signature(payload, signature, secret):
            raise WebhookSignatureError("Invalid webhook signature")
        try:
            data = json.loads(payload)
        except json.JSONDecodeError as e:
            raise MyRoPayError(f"Invalid JSON in webhook: {e}") from e
        return APIResponse(_data=data)


# ---- Main client ----
class Client:
    """
    MyRoPay API client.

    client = myropay.Client("sk_live_...")
    """

    def __init__(self, secret_key: str):
        if not secret_key:
            raise ValueError("secret_key is required")
        http = _HttpClient(secret_key)
        self.sessions      = Sessions(http)
        self.refunds       = Refunds(http)
        self.subscriptions = Subscriptions(http)
        self.disputes      = Disputes(http)
        self.apps          = Apps(http)

    @staticmethod
    def construct_webhook_event(
        payload: bytes,
        signature: str,
        webhook_secret: str,
    ) -> APIResponse:
        """Verify and parse an incoming webhook. Convenience alias for Webhook.construct_event."""
        return Webhook.construct_event(payload, signature, webhook_secret)
