"""Paymob checkout initiator + HMAC-verified webhook.

This module talks to the `Paymob Accept <https://accept.paymob.com/>`_ API:

1. ``POST /api/payments/paymob/checkout``
   Authenticates with the merchant API key, registers an order, and
   generates a payment key. Returns the ``payment_key`` + the hosted iframe
   URL the frontend should redirect the user to.

2. ``POST /api/payments/paymob/webhook``
   Verifies Paymob's HMAC-SHA512 signature against a concatenated subset
   of the transaction fields (per Paymob docs), marks the matching
   ``encoach.paymob.order`` as ``paid`` or ``failed``, and idempotently
   no-ops on duplicate deliveries.

Configuration (via ``ir.config_parameter``):

* ``encoach.paymob.api_key``            — merchant API key
* ``encoach.paymob.hmac_secret``        — HMAC secret (from the Paymob dashboard)
* ``encoach.paymob.integration_id``     — default integration id (card)
* ``encoach.paymob.iframe_id``          — hosted iframe id for redirect URL

All secrets are read at request time so operators can rotate them without
restarting Odoo.
"""

from __future__ import annotations

import hashlib
import hmac
import json
import logging
from typing import Any

import requests

from odoo import fields, http
from odoo.http import Response, request

from odoo.addons.encoach_api.controllers.base import (
    _error_response,
    _get_json_body,
    _json_response,
    jwt_required,
)

_logger = logging.getLogger(__name__)

PAYMOB_BASE = "https://accept.paymob.com/api"

# Fields Paymob concatenates (in this exact order) to compute the HMAC we
# receive on webhook callbacks. Source: Paymob Accept docs, Transaction
# Processed Callback → "HMAC Calculation" section.
HMAC_FIELDS = [
    "amount_cents",
    "created_at",
    "currency",
    "error_occured",
    "has_parent_transaction",
    "id",
    "integration_id",
    "is_3d_secure",
    "is_auth",
    "is_capture",
    "is_refunded",
    "is_standalone_payment",
    "is_voided",
    "order.id",
    "owner",
    "pending",
    "source_data.pan",
    "source_data.sub_type",
    "source_data.type",
    "success",
]


def _config(key: str, default: str = "") -> str:
    return (
        request.env["ir.config_parameter"].sudo().get_param(f"encoach.paymob.{key}", default)
        or ""
    )


def _paymob_post(path: str, body: dict, timeout: int = 15) -> dict:
    url = f"{PAYMOB_BASE}{path}"
    resp = requests.post(url, json=body, timeout=timeout)
    if resp.status_code >= 400:
        raise RuntimeError(f"Paymob {path} → {resp.status_code}: {resp.text[:300]}")
    try:
        return resp.json()
    except ValueError as exc:
        raise RuntimeError(f"Paymob {path} returned non-JSON: {resp.text[:300]}") from exc


def _get_nested(obj: dict, dotted: str) -> Any:
    """Walk 'a.b.c' through nested dicts, returning '' on any miss."""
    cur: Any = obj
    for part in dotted.split("."):
        if not isinstance(cur, dict):
            return ""
        cur = cur.get(part, "")
    return cur if cur is not None else ""


def _compute_hmac(obj: dict, secret: str) -> str:
    """Reproduce Paymob's HMAC-SHA512 over the canonical field sequence."""
    parts = [str(_get_nested(obj, f)).lower() if isinstance(_get_nested(obj, f), bool)
             else str(_get_nested(obj, f))
             for f in HMAC_FIELDS]
    message = "".join(parts).encode("utf-8")
    return hmac.new(secret.encode("utf-8"), message, hashlib.sha512).hexdigest()


class EncoachPaymobController(http.Controller):
    """Checkout + webhook endpoints."""

    # ------------------------------------------------------------------
    # POST /api/payments/paymob/checkout
    #
    # Body: { amount_cents, currency, product_ref?, description?, integration_id? }
    # ------------------------------------------------------------------
    @http.route(
        "/api/payments/paymob/checkout",
        type="http", auth="none", methods=["POST"], csrf=False,
    )
    @jwt_required
    def checkout(self, **kw):
        try:
            api_key = _config("api_key")
            iframe_id = _config("iframe_id")
            default_integration = _config("integration_id")
            if not api_key or not iframe_id or not default_integration:
                return _error_response(
                    "Paymob is not configured. "
                    "Set encoach.paymob.api_key / hmac_secret / integration_id / iframe_id "
                    "in System Parameters.",
                    503,
                )

            body = _get_json_body() or {}
            try:
                amount_cents = int(body.get("amount_cents") or 0)
            except (TypeError, ValueError):
                return _error_response("amount_cents must be an integer", 400)
            if amount_cents <= 0:
                return _error_response("amount_cents must be positive", 400)

            currency = (body.get("currency") or "EGP").upper()
            description = body.get("description") or ""
            product_ref = body.get("product_ref") or ""
            integration_id = str(body.get("integration_id") or default_integration)

            user = request.env.user
            partner = user.partner_id

            # Create our own tracking row *before* calling Paymob so we never
            # lose orders to partial failures.
            order = request.env["encoach.paymob.order"].sudo().create({
                "reference": f"enc-{user.id}-{int(fields.Datetime.now().timestamp())}",
                "user_id": user.id,
                "partner_id": partner.id if partner else False,
                "amount_cents": amount_cents,
                "currency": currency,
                "description": description,
                "product_ref": product_ref,
                "integration_id": integration_id,
                "state": "draft",
            })

            # Step 1 — authenticate with the merchant API key.
            auth = _paymob_post("/auth/tokens", {"api_key": api_key})
            auth_token = auth.get("token")
            if not auth_token:
                raise RuntimeError("Paymob auth returned no token")

            # Step 2 — register the order with Paymob.
            order_payload = {
                "auth_token": auth_token,
                "delivery_needed": False,
                "amount_cents": amount_cents,
                "currency": currency,
                "items": [],
                "merchant_order_id": order.reference,
            }
            paymob_order = _paymob_post("/ecommerce/orders", order_payload)
            paymob_order_id = str(paymob_order.get("id") or "")
            if not paymob_order_id:
                raise RuntimeError("Paymob order registration returned no id")

            # Step 3 — generate a payment key.
            billing = {
                "first_name": (user.name or "Guest").split(" ")[0][:50] or "Guest",
                "last_name": (
                    " ".join((user.name or "User").split(" ")[1:])[:50] or "User"
                ),
                "email": user.email or "no-reply@encoach.invalid",
                "phone_number": partner.phone or partner.mobile or "+201000000000",
                "apartment": "NA", "floor": "NA", "street": "NA",
                "building": "NA", "shipping_method": "NA", "postal_code": "NA",
                "city": "NA", "country": "EG", "state": "NA",
            }
            pk_payload = {
                "auth_token": auth_token,
                "amount_cents": amount_cents,
                "currency": currency,
                "order_id": paymob_order_id,
                "billing_data": billing,
                "integration_id": int(integration_id),
                "lock_order_when_paid": True,
            }
            pk = _paymob_post("/acceptance/payment_keys", pk_payload)
            payment_key = pk.get("token")
            if not payment_key:
                raise RuntimeError("Paymob payment_keys returned no token")

            iframe_url = (
                f"https://accept.paymob.com/api/acceptance/iframes/"
                f"{iframe_id}?payment_token={payment_key}"
            )

            order.write({
                "paymob_order_id": paymob_order_id,
                "payment_key": payment_key,
                "state": "initiated",
            })

            return _json_response({
                "order_id": order.id,
                "reference": order.reference,
                "paymob_order_id": paymob_order_id,
                "payment_key": payment_key,
                "iframe_url": iframe_url,
            })
        except Exception as e:
            _logger.exception("paymob checkout failed")
            return _error_response(str(e), 500)

    # ------------------------------------------------------------------
    # POST /api/payments/paymob/webhook
    #
    # Paymob delivers the full transaction JSON with an ``hmac`` query-string
    # parameter. We verify the HMAC over a canonical subset of fields and
    # then update the matching order.
    # ------------------------------------------------------------------
    @http.route(
        "/api/payments/paymob/webhook",
        type="http", auth="public", methods=["POST"], csrf=False,
    )
    def webhook(self, **kw):
        try:
            secret = _config("hmac_secret")
            if not secret:
                _logger.error("paymob webhook: hmac secret not configured")
                return Response(status=503)

            received_hmac = (kw.get("hmac") or "").strip()
            if not received_hmac:
                return Response(status=400)

            raw = (request.httprequest.data or b"")
            try:
                payload = json.loads(raw.decode("utf-8") or "{}")
            except Exception:
                return Response(status=400)

            # Paymob's payload envelope is:
            # { "type": "TRANSACTION", "obj": { ...fields... } }
            inner = payload.get("obj") or payload

            expected_hmac = _compute_hmac(inner, secret)
            if not hmac.compare_digest(expected_hmac, received_hmac):
                _logger.warning(
                    "paymob webhook: HMAC mismatch (expected=%s… got=%s…)",
                    expected_hmac[:16], received_hmac[:16],
                )
                return Response(status=401)

            merchant_order_ref = (
                _get_nested(inner, "order.merchant_order_id")
                or inner.get("merchant_order_id")
                or ""
            )
            paymob_order_id = str(_get_nested(inner, "order.id") or "")
            success = bool(inner.get("success"))

            Order = request.env["encoach.paymob.order"].sudo()
            order = False
            if merchant_order_ref:
                order = Order.search([("reference", "=", merchant_order_ref)], limit=1)
            if not order and paymob_order_id:
                order = Order.search([("paymob_order_id", "=", paymob_order_id)], limit=1)
            if not order:
                _logger.warning(
                    "paymob webhook: no matching order for ref=%s paymob=%s",
                    merchant_order_ref, paymob_order_id,
                )
                # Return 200 so Paymob doesn't retry indefinitely — the event
                # will still be visible in their dashboard.
                return Response(status=200)

            if order.state == "paid":
                # Idempotent: duplicate delivery.
                return Response(status=200)

            payload_json = json.dumps(inner)
            if success:
                order.mark_paid(payload_json, received_hmac)
            else:
                order.mark_failed(payload_json, received_hmac)

            return Response(status=200)
        except Exception:
            _logger.exception("paymob webhook handler crashed")
            return Response(status=500)

    # ------------------------------------------------------------------
    # GET /api/payments/paymob/orders — list my orders
    # ------------------------------------------------------------------
    @http.route(
        "/api/payments/paymob/orders",
        type="http", auth="none", methods=["GET"], csrf=False,
    )
    @jwt_required
    def my_orders(self, **kw):
        try:
            Order = request.env["encoach.paymob.order"].sudo()
            domain = [("user_id", "=", request.env.user.id)]
            rows = Order.search(domain, order="create_date desc", limit=200)
            items = [{
                "id": r.id,
                "reference": r.reference,
                "paymob_order_id": r.paymob_order_id or "",
                "amount_cents": r.amount_cents,
                "currency": r.currency,
                "description": r.description or "",
                "product_ref": r.product_ref or "",
                "state": r.state,
                "create_date": fields.Datetime.to_string(r.create_date) if r.create_date else None,
                "last_event_at": (
                    fields.Datetime.to_string(r.last_event_at) if r.last_event_at else None
                ),
            } for r in rows]
            return _json_response({
                "items": items,
                "data": items,
                "total": len(items),
            })
        except Exception as e:
            _logger.exception("paymob my_orders failed")
            return _error_response(str(e), 500)
