import asyncio
import hashlib
import json
import os
import time
import urllib.request
import uuid

import websockets

# ── BIP340 Schnorr (pure Python) ──────────────────────────────────────────────
_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
_G = (
    0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
    0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
)


def _point_add(P, Q):
    if P is None: return Q
    if Q is None: return P
    if P[0] == Q[0] and P[1] != Q[1]: return None
    if P == Q:
        lam = (3 * P[0] * P[0] * pow(2 * P[1], _P - 2, _P)) % _P
    else:
        lam = ((Q[1] - P[1]) * pow(Q[0] - P[0], _P - 2, _P)) % _P
    x = (lam * lam - P[0] - Q[0]) % _P
    return (x, (lam * (P[0] - x) - P[1]) % _P)


def _point_mul(P, n):
    R = None
    for i in range(256):
        if (n >> i) & 1:
            R = _point_add(R, P)
        P = _point_add(P, P)
    return R


def _tagged_hash(tag: str, msg: bytes) -> bytes:
    h = hashlib.sha256(tag.encode()).digest()
    return hashlib.sha256(h + h + msg).digest()


def schnorr_sign(msg: bytes, seckey_bytes: bytes) -> bytes:
    d0 = int.from_bytes(seckey_bytes, "big")
    P = _point_mul(_G, d0)
    d = d0 if P[1] % 2 == 0 else _N - d0
    t = (d ^ int.from_bytes(_tagged_hash("BIP0340/aux", os.urandom(32)), "big")).to_bytes(32, "big")
    rand = _tagged_hash("BIP0340/nonce", t + P[0].to_bytes(32, "big") + msg)
    k0 = int.from_bytes(rand, "big") % _N
    R = _point_mul(_G, k0)
    k = k0 if R[1] % 2 == 0 else _N - k0
    e = int.from_bytes(
        _tagged_hash("BIP0340/challenge", R[0].to_bytes(32, "big") + P[0].to_bytes(32, "big") + msg), "big"
    ) % _N
    return R[0].to_bytes(32, "big") + ((k + e * d) % _N).to_bytes(32, "big")


def pubkey_from_seckey(seckey_bytes: bytes) -> str:
    P = _point_mul(_G, int.from_bytes(seckey_bytes, "big"))
    return P[0].to_bytes(32, "big").hex()


# ── NostrClient ───────────────────────────────────────────────────────────────

class NostrClient:
    def __init__(self, default_relays: list[str] | None = None):
        self._connections: dict[str, websockets.WebSocketClientProtocol] = {}
        self._default_relays = list(default_relays or [])

    def resolve_urls(self, url: str | list[str] | None) -> list[str]:
        """Resolves the url argument to a relay list, falling back to config relays."""
        if url is None:
            if not self._default_relays:
                raise ValueError("No relay given and no relays configured in config.json.")
            return list(self._default_relays)
        return [url] if isinstance(url, str) else list(url)

    async def connect(self, url: str) -> str:
        if url in self._connections:
            return f"Już połączono z '{url}'."
        ws = await websockets.connect(url)
        self._connections[url] = ws
        return f"Połączono z '{url}'."

    async def _ensure_connected(self, url: str):
        """Lazily opens a connection so config relays don't need an explicit connect_relay."""
        ws = self._connections.get(url)
        if ws is None:
            ws = await websockets.connect(url)
            self._connections[url] = ws
        return ws

    async def disconnect(self, url: str) -> str:
        ws = self._connections.pop(url, None)
        if ws is None:
            return f"Brak połączenia z '{url}'."
        await ws.close()
        return f"Rozłączono '{url}'."

    async def get_events(self, url: str | list[str] | None, filter: dict, aliases=None) -> list:
        urls = self.resolve_urls(url)
        resolved_filter = aliases.resolve_in_filter(filter) if aliases else filter
        merged: dict[str, dict] = {}
        for u in urls:
            for ev in await self._query_one(u, resolved_filter):
                merged[ev["id"]] = ev
        events = list(merged.values())
        if aliases:
            events = [aliases.replace_in_data(ev) for ev in events]
        return events

    async def _query_one(self, url: str, resolved_filter: dict) -> list:
        ws = await self._ensure_connected(url)
        sub_id = uuid.uuid4().hex[:8]
        await ws.send(json.dumps(["REQ", sub_id, resolved_filter]))
        events = []
        try:
            async with asyncio.timeout(10):
                async for raw in ws:
                    msg = json.loads(raw)
                    if msg[0] == "EVENT" and msg[1] == sub_id:
                        events.append(msg[2])
                    elif msg[0] == "EOSE":
                        break
        finally:
            await ws.send(json.dumps(["CLOSE", sub_id]))
        return events

    def _build_event(self, pubkey: str, kind: int, content: str, tags: list) -> dict:
        created_at = int(time.time())
        serialized = json.dumps(
            [0, pubkey, created_at, kind, tags, content],
            separators=(",", ":"),
            ensure_ascii=False,
        )
        event_id = hashlib.sha256(serialized.encode("utf-8")).hexdigest()
        return {
            "id": event_id,
            "pubkey": pubkey,
            "created_at": created_at,
            "kind": kind,
            "tags": tags,
            "content": content,
        }

    def _sign_event(self, event: dict, seckey_hex: str) -> dict:
        sig = schnorr_sign(bytes.fromhex(event["id"]), bytes.fromhex(seckey_hex))
        return {**event, "sig": sig.hex()}

    async def publish(self, url: str | list[str] | None, unsigned: dict, pubkey: str, privkey: str, aliases=None) -> str:
        urls = self.resolve_urls(url)
        kind = int(unsigned.get("kind", 1))
        content = str(unsigned.get("content", ""))
        tags = unsigned.get("tags", [])
        if aliases:
            tags = aliases.resolve_in_tags(tags)

        # Build and sign once — the same event is broadcast to every relay.
        event = self._build_event(pubkey, kind, content, tags)
        event = self._sign_event(event, privkey)

        statuses = [f"{u}: {await self._send_one(u, event)}" for u in urls]
        label = aliases.get_or_create(event["id"], "e") if aliases else event["id"][:16] + "…"
        return f"Event {label}\n" + "\n".join(statuses)

    async def _send_one(self, url: str, event: dict) -> str:
        ws = await self._ensure_connected(url)
        await ws.send(json.dumps(["EVENT", event]))
        try:
            async with asyncio.timeout(5):
                async for raw in ws:
                    msg = json.loads(raw)
                    if msg[0] == "OK" and msg[1] == event["id"]:
                        if msg[2]:
                            return "opublikowano"
                        return f"odrzucono ({msg[3] if len(msg) > 3 else 'brak powodu'})"
        except TimeoutError:
            pass
        return "wysłano (brak potwierdzenia)"

    async def check_relays(self, urls: list[str], filter: dict | None = None) -> list:
        results = []
        for url in urls:
            try:
                info = await self._fetch_nip11(url)
                nips = info.get("supported_nips", [])
                limitation = info.get("limitation", {})
                flat = {
                    "url": url,
                    "name": info.get("name", ""),
                    "payment_required": limitation.get("payment_required", False),
                    "auth_required": limitation.get("auth_required", False),
                    "supported_nips": nips,
                }

                if filter:
                    required_nips = filter.get("required_nips")
                    if required_nips and not all(n in nips for n in required_nips):
                        continue
                    if not self._matches_filter(flat, {k: v for k, v in filter.items() if k != "required_nips"}):
                        continue

                results.append(flat)
            except Exception as e:
                results.append({"url": url, "error": str(e)})
        return results

    def _matches_filter(self, data: dict, filter: dict) -> bool:
        for key, expected in filter.items():
            if data.get(key) != expected:
                return False
        return True

    async def _fetch_nip11(self, url: str) -> dict:
        https_url = url.rstrip("/").replace("wss://", "https://").replace("ws://", "http://")
        req = urllib.request.Request(https_url, headers={"Accept": "application/nostr+json"})
        loop = asyncio.get_event_loop()

        def do_fetch():
            with urllib.request.urlopen(req, timeout=5) as resp:
                return json.loads(resp.read())

        return await loop.run_in_executor(None, do_fetch)
