"""Neptuv SDK for Python — https://neptuv.xyz/docs

Two jobs in one file (copy it into your project):

  * Agents: sign each outgoing request with the agent's registered wallet, so platforms
    can prove who is calling (Know Your Agent).
  * Platforms: look up trust scores, apply your policy, and verify signed requests.

Requires:  pip install eth-account requests

Agent side:
    from neptuv import sign_request
    headers = sign_request(AGENT_PRIVATE_KEY, "eip155:8453:1203", "POST", url, body)
    requests.post(url, data=body, headers={**headers, "Content-Type": "application/json"})

Platform side (Flask):
    from neptuv import Neptuv, require_trusted_agent
    neptuv = Neptuv(api_key=os.environ["NEPTUV_API_KEY"])

    @app.post("/orders")
    @require_trusted_agent(neptuv)            # 401/403 unless the agent is verified and allowed
    def create_order():
        agent = flask.g.neptuv["agent"]       # score, label, ...
"""
from __future__ import annotations

import hashlib
import secrets
import time
from functools import wraps
from urllib.parse import urlsplit, urlunsplit

import requests
from eth_account import Account
from eth_account.messages import encode_defunct

__version__ = "1.0.0"
PROTOCOL = "neptuv-agent-auth/1"
HEADER_NAMES = ("Neptuv-Agent", "Neptuv-Timestamp", "Neptuv-Nonce", "Neptuv-Signature")
CHAIN_IDS = {"ethereum": 1, "base": 8453, "arbitrum": 42161, "optimism": 10, "polygon": 137, "bnb": 56,
             "base-sepolia": 84532, "sepolia": 11155111}


class NeptuvError(Exception):
    def __init__(self, message, status=None, payload=None):
        super().__init__(message)
        self.status = status
        self.payload = payload


def canonical_agent(agent: str) -> str:
    """'base:1203', '8453:1203' or 'eip155:8453:0x8004…:1203' → 'eip155:8453:1203'."""
    parts = str(agent).strip().split(":")
    if len(parts) == 4 and parts[0] == "eip155":
        chain, agent_id = parts[1], parts[3]
    elif len(parts) == 3 and parts[0] == "eip155":
        chain, agent_id = parts[1], parts[2]
    elif len(parts) == 2:
        chain, agent_id = parts
    else:
        raise ValueError("agent must look like eip155:8453:1203")
    chain_id = CHAIN_IDS.get(chain.lower(), chain)
    if not str(chain_id).isdigit() or not str(agent_id).isdigit():
        raise ValueError("agent must look like eip155:8453:1203")
    return f"eip155:{int(chain_id)}:{int(agent_id)}"


def normalize_url(url: str) -> str:
    parts = urlsplit(str(url).strip())
    scheme, host, port = parts.scheme.lower(), (parts.hostname or "").lower(), parts.port
    if scheme not in ("http", "https") or not host:
        raise ValueError("url must be a full http(s) URL")
    if ":" in host:
        host = f"[{host}]"
    netloc = host if port is None or (scheme, port) in (("https", 443), ("http", 80)) else f"{host}:{port}"
    return urlunsplit((scheme, netloc, parts.path or "/", parts.query, ""))


def body_sha256(body=b"") -> str:
    if body is None:
        body = b""
    if isinstance(body, str):
        body = body.encode("utf-8")
    return hashlib.sha256(body).hexdigest()


def canonical_message(agent, method, url, timestamp, nonce, body_hash) -> str:
    return "\n".join([PROTOCOL, f"agent: {canonical_agent(agent)}", f"method: {method.upper()}",
                      f"url: {normalize_url(url)}", f"timestamp: {int(timestamp)}", f"nonce: {nonce}",
                      f"body-sha256: {body_hash.lower()}"])


def sign_request(private_key, agent: str, method: str, url: str, body=b"", timestamp=None, nonce=None) -> dict:
    """Headers to attach to an outgoing request. The key must be the agent's agentWallet (or its owner)."""
    agent = canonical_agent(agent)
    timestamp = int(timestamp or time.time())
    nonce = nonce or secrets.token_urlsafe(18)
    message = canonical_message(agent, method, url, timestamp, nonce, body_sha256(body))
    signature = Account.sign_message(encode_defunct(text=message), private_key).signature.hex()
    return {"Neptuv-Agent": agent, "Neptuv-Timestamp": str(timestamp), "Neptuv-Nonce": nonce,
            "Neptuv-Signature": signature if signature.startswith("0x") else "0x" + signature}


class Neptuv:
    """Client for the Neptuv API."""

    def __init__(self, api_key: str | None = None, base_url: str = "https://neptuv.xyz/api/v1", timeout: float = 10,
                 session=None):
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.session = session or requests.Session()

    def _request(self, method, path, **kwargs):
        headers = kwargs.pop("headers", {})
        if self.api_key:
            headers["Authorization"] = f"Bearer {self.api_key}"
        resp = self.session.request(method, self.base_url + path, headers=headers, timeout=self.timeout, **kwargs)
        try:
            data = resp.json()
        except ValueError:
            data = {"error": resp.text[:200]}
        if resp.status_code >= 400:
            raise NeptuvError(data.get("error") or f"HTTP {resp.status_code}", resp.status_code, data)
        return data

    def agent(self, chain: str, agent_id) -> dict:
        return self._request("GET", f"/agents/{chain}/{agent_id}")

    def score(self, chain: str, agent_id) -> dict:
        return self._request("GET", f"/agents/{chain}/{agent_id}/score")

    def search(self, **params) -> dict:
        return self._request("GET", "/search", params=params)

    def gate(self, agent: str, policy_id=None, **rules) -> dict:
        return self._request("POST", "/gate", json={"agent": canonical_agent(agent), "policy": policy_id, **rules})

    def attest(self, agent: str, ttl: int = 3600) -> dict:
        return self._request("POST", "/attest", json={"agent": canonical_agent(agent), "ttl": ttl})

    def verify(self, method: str, url: str, headers, body=b"", policy_id=None, attest: bool = False, **rules) -> dict:
        """Verify a signed agent request you received. Pass the request's own method, URL, headers and raw body."""
        lower = {k.lower(): v for k, v in dict(headers).items()}
        payload = {"method": method, "url": url, "body_sha256": body_sha256(body), "policy": policy_id,
                   "attest": attest, "headers": {name: lower.get(name.lower()) for name in HEADER_NAMES}, **rules}
        return self._request("POST", "/verify", json=payload)


def require_trusted_agent(client: Neptuv, policy_id=None, **rules):
    """Flask decorator: only let verified, allowed agents through. Puts Neptuv's answer in flask.g.neptuv."""
    def decorator(view):
        @wraps(view)
        def wrapper(*args, **kwargs):
            from flask import g, jsonify, request
            if not request.headers.get("Neptuv-Signature"):
                return jsonify(error="This endpoint accepts verified AI agents only (Neptuv-* headers missing)."), 401
            try:
                result = client.verify(request.method, request.url, request.headers, request.get_data(),
                                       policy_id=policy_id, **rules)
            except NeptuvError as exc:
                return jsonify(error=f"Agent verification unavailable: {exc}"), 503
            g.neptuv = result
            if not result.get("verified"):
                return jsonify(error="Agent signature not verified.", reasons=result.get("reasons")), 401
            if not result.get("allow"):
                return jsonify(error="Agent not allowed by policy.", reasons=result.get("reasons")), 403
            return view(*args, **kwargs)
        return wrapper
    return decorator
