"""
CA Automator — payment gateway integration (Khalti, ConnectIPS).

Both gateways are entirely optional and entirely inert until a super admin
fills in real credentials from Admin > Site settings > Payment gateways —
same posture as CA Assistant needing ANTHROPIC_API_KEY or email needing
SMTP config: every function here checks configured() first and raises a
plain, expected PaymentError (never a 500) when it isn't. No credential is
ever hardcoded, mocked, or defaulted to a "test" value that would look like
it works — an unconfigured gateway simply cannot be initiated.

Khalti (ePayment API v2): a straightforward JSON/HTTPS API, no extra
dependency — stdlib urllib.request is enough. initiate() gets a payment_url
to redirect the payer to; verify() looks the transaction up server-side
after they return, which is the only status check ever trusted (never the
redirect's own query string).

ConnectIPS: a bank-mediated, certificate-based flow used by Nepali banks —
structurally different from Khalti. The merchant redirects the payer to
ConnectIPS with a set of fields plus a TOKEN: an RSA-SHA256 signature (over
a specific comma-joined subset of those fields) produced with a private key
held in a bank-issued PKCS12 (.pfx) certificate. That signing step needs
real asymmetric crypto, which the standard library doesn't provide — hence
the one new optional dependency (`cryptography`, see requirements.txt), used
only inside this file and only when a firm has actually configured
ConnectIPS. This implementation follows ConnectIPS's published v2 field
/ signing spec; it has not been exercised against a live bank UAT
environment (that requires bank-issued sandbox credentials this codebase
has no access to) — treat it as structurally correct and ready to verify
against real credentials, not as already-proven-working.
"""
import base64
import hashlib
import json
import secrets
import urllib.error
import urllib.request
from datetime import datetime, timezone

import db

try:
    from cryptography.hazmat.primitives.serialization import pkcs12
    from cryptography.hazmat.primitives.asymmetric import padding
    from cryptography.hazmat.primitives import hashes
    _CRYPTO_AVAILABLE = True
except ImportError:
    _CRYPTO_AVAILABLE = False


class PaymentError(Exception):
    """Always a plain, user-facing message — never a stack trace. Routes
    catch this and return it as a 400, the same way HttpError works."""
    pass


def _config():
    return db.query("SELECT * FROM payment_gateway_config ORDER BY firm_id LIMIT 1", one=True)


def _gen_ref(prefix):
    return f"{prefix}-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}-{secrets.token_hex(4)}"


def _post_json(url, payload, headers, timeout=20):
    req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"),
                                 headers=dict(headers, **{"Content-Type": "application/json"}), method="POST")
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", "replace")
        try:
            detail = json.loads(body)
        except Exception:
            detail = body
        raise PaymentError(f"Payment gateway rejected the request: {detail}")
    except urllib.error.URLError as e:
        raise PaymentError(f"Could not reach the payment gateway: {e.reason}")


# ------------------------------------------------------------------- khalti

def khalti_configured():
    cfg = _config()
    return bool(cfg and cfg["khalti_secret_key_enc"])


def khalti_initiate(amount_npr, purchase_order_id, purchase_order_name, return_url, website_url,
                    customer_name=None, customer_email=None, customer_phone=None):
    """amount_npr is NPR (rupees); Khalti's API takes paisa (amount * 100).
    Returns {"pidx": ..., "payment_url": ...} — redirect the payer to
    payment_url. Raises PaymentError if Khalti isn't configured or the
    request itself fails."""
    cfg = _config()
    if not cfg or not cfg["khalti_secret_key_enc"]:
        raise PaymentError("Khalti is not configured yet — ask your firm's admin to set it up in Site settings.")
    secret = db.decrypt_secret(cfg["khalti_secret_key_enc"])
    base = "https://khalti.com/api/v2/epayment/" if cfg["khalti_mode"] == "live" else "https://dev.khalti.com/api/v2/epayment/"
    payload = {
        "return_url": return_url, "website_url": website_url,
        "amount": int(round(amount_npr * 100)),
        "purchase_order_id": purchase_order_id, "purchase_order_name": purchase_order_name,
    }
    customer_info = {k: v for k, v in
                     {"name": customer_name, "email": customer_email, "phone": customer_phone}.items() if v}
    if customer_info:
        payload["customer_info"] = customer_info
    return _post_json(base + "initiate/", payload, {"Authorization": "Key " + secret})


def khalti_verify(pidx):
    """Server-side lookup — the only source of truth for whether a Khalti
    payment actually succeeded. Returns Khalti's own response dict; check
    result["status"] == "Completed"."""
    cfg = _config()
    if not cfg or not cfg["khalti_secret_key_enc"]:
        raise PaymentError("Khalti is not configured")
    secret = db.decrypt_secret(cfg["khalti_secret_key_enc"])
    base = "https://khalti.com/api/v2/epayment/" if cfg["khalti_mode"] == "live" else "https://dev.khalti.com/api/v2/epayment/"
    return _post_json(base + "lookup/", {"pidx": pidx}, {"Authorization": "Key " + secret})


# --------------------------------------------------------------- connectips

def connectips_configured():
    cfg = _config()
    return bool(cfg and _CRYPTO_AVAILABLE and cfg["connectips_merchant_id"] and cfg["connectips_app_id"]
               and cfg["connectips_cert_path"])


def _connectips_private_key(cfg):
    if not _CRYPTO_AVAILABLE:
        raise PaymentError("ConnectIPS support needs the 'cryptography' package installed on the server.")
    cert_pass = db.decrypt_secret(cfg["connectips_cert_pass_enc"]) if cfg["connectips_cert_pass_enc"] else None
    try:
        with open(cfg["connectips_cert_path"], "rb") as f:
            pfx_bytes = f.read()
        private_key, _cert, _chain = pkcs12.load_key_and_certificates(
            pfx_bytes, cert_pass.encode("utf-8") if cert_pass else None)
    except Exception as e:
        raise PaymentError(f"Could not read the ConnectIPS certificate: {e}")
    if private_key is None:
        raise PaymentError("The ConnectIPS certificate file has no private key in it")
    return private_key


def _connectips_sign(cfg, message):
    key = _connectips_private_key(cfg)
    signature = key.sign(message.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256())
    return base64.b64encode(signature).decode("ascii")


def connectips_build_redirect(amount_npr, txn_id, reference_id, remarks, particulars, return_url):
    """Returns {"action_url": ..., "fields": {...}} — the caller renders
    this as an auto-submitting HTML form (ConnectIPS is a redirect/POST
    flow, not a JSON API for the initial step) and the payer's browser ends
    up on ConnectIPS's own hosted payment page."""
    cfg = _config()
    if not cfg or not cfg["connectips_merchant_id"]:
        raise PaymentError("ConnectIPS is not configured yet — ask your firm's admin to set it up in Site settings.")
    txn_date = datetime.now(timezone.utc).strftime("%d-%m-%Y")
    txn_amt = f"{amount_npr:.2f}"
    fields = {
        "MERCHANTID": cfg["connectips_merchant_id"], "APPID": cfg["connectips_app_id"],
        "APPNAME": cfg["connectips_app_name"] or "", "TXNID": txn_id, "TXNDATE": txn_date,
        "TXNCRNCY": "NPR", "TXNAMT": txn_amt, "REFERENCEID": reference_id,
        "REMARKS": remarks or "", "PARTICULARS": particulars or "",
    }
    message = ",".join([fields["MERCHANTID"], fields["APPID"], fields["APPNAME"], fields["TXNID"],
                        fields["TXNDATE"], fields["TXNCRNCY"], fields["TXNAMT"], fields["REFERENCEID"],
                        fields["REMARKS"], fields["PARTICULARS"]])
    fields["TOKEN"] = _connectips_sign(cfg, message)
    action = "https://connectips.com/connectipswebgw/loginpage" if cfg["connectips_mode"] == "live" \
        else "https://uat.connectips.com/connectipswebgw/loginpage"
    return {"action_url": action, "fields": fields}


def connectips_verify(txn_id, reference_id, amount_npr):
    """Calls ConnectIPS's transaction-validation API server-side after the
    payer returns — the only status check ever trusted, same discipline as
    khalti_verify(). Returns the gateway's parsed JSON response."""
    cfg = _config()
    if not cfg or not cfg["connectips_merchant_id"]:
        raise PaymentError("ConnectIPS is not configured")
    txn_date = datetime.now(timezone.utc).strftime("%d-%m-%Y")
    txn_amt = f"{amount_npr:.2f}"
    message = ",".join([cfg["connectips_merchant_id"], cfg["connectips_app_id"], cfg["connectips_app_name"] or "",
                        txn_id, txn_date, "NPR", txn_amt, reference_id])
    token = _connectips_sign(cfg, message)
    payload = {
        "merchantId": cfg["connectips_merchant_id"], "appId": cfg["connectips_app_id"],
        "appName": cfg["connectips_app_name"] or "", "txnId": txn_id, "txnDate": txn_date,
        "txnCrncy": "NPR", "txnAmt": txn_amt, "referenceId": reference_id, "token": token,
    }
    base = "https://connectips.com/connectipswebws/api/creditor/validatetxn" if cfg["connectips_mode"] == "live" \
        else "https://uat.connectips.com/connectipswebws/api/creditor/validatetxn"
    password = db.decrypt_secret(cfg["connectips_password_enc"]) if cfg["connectips_password_enc"] else ""
    auth = base64.b64encode(f"{cfg['connectips_app_id']}:{password}".encode("utf-8")).decode("ascii")
    return _post_json(base, payload, {"Authorization": "Basic " + auth})
