"""
Phase 23: Federation — Cross-Instance Agent Marketplace.

Brynq instances discover and connect to each other. Agents build portable
reputation that travels across workspaces. Any organization can run a Brynq
instance; they all interoperate via the federation protocol.

Key concepts:
  - INSTANCE: A running Brynq broker with a unique instance_id and public URL.
  - PEER: Another Brynq instance this one has established trust with.
  - TRUST RECEIPT: A cryptographically signed record of task completion that
    an agent can present to any instance as proof of reputation.
  - FEDERATION REGISTRY: Directory of known Brynq instances and their agents.
  - CROSS-INSTANCE HIRE: Post a job on Instance A, an agent from Instance B
    applies and gets hired — reputation transfers.

Storage:
  state/broker/federation/
    instance.json           — this instance's identity and config
    peers/{peer_id}.json    — known peer instances
    receipts/{receipt_id}.json — trust receipts (portable reputation)
    registry.jsonl          — federation event log
"""

import hashlib
import json
import os
import secrets
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    CHANNELS_DIR,
    SESSION_ID,
    SESSION_NAME,
    PLATFORM,
    _append_jsonl_sync,
    _write_json_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

FEDERATION_DIR = BROKER_DIR / "federation"
PEERS_DIR = FEDERATION_DIR / "peers"
RECEIPTS_DIR = FEDERATION_DIR / "receipts"
INSTANCE_FILE = FEDERATION_DIR / "instance.json"
REGISTRY_LOG = FEDERATION_DIR / "registry.jsonl"

for _d in (FEDERATION_DIR, PEERS_DIR, RECEIPTS_DIR):
    _d.mkdir(parents=True, exist_ok=True)


# ── Instance Identity ──────────────────────────────────────────────────────


def init_instance(
    name: str = "",
    public_url: str = "",
    description: str = "",
) -> dict:
    """Initialize or update this Brynq instance's federation identity.

    Called once on first federation setup. Generates a unique instance_id
    and a signing secret for trust receipts.
    """
    existing = get_instance()

    instance_id = existing.get("instance_id") or uuid.uuid4().hex[:16]
    signing_secret = existing.get("signing_secret") or secrets.token_hex(32)

    instance = {
        "instance_id": instance_id,
        "name": name or existing.get("name", f"brynq-{instance_id[:8]}"),
        "public_url": public_url or existing.get("public_url", ""),
        "description": description or existing.get("description", ""),
        "signing_secret": signing_secret,
        "created": existing.get("created", datetime.now(timezone.utc).isoformat()),
        "updated": datetime.now(timezone.utc).isoformat(),
        "agent_count": 0,
        "task_count": 0,
        "federation_status": "active",
    }

    _write_json_sync(INSTANCE_FILE, instance)
    _log_event("init", f"Instance {instance_id} initialized: {instance['name']}")
    return instance


def get_instance() -> dict:
    """Get this instance's federation identity."""
    try:
        return json.loads(INSTANCE_FILE.read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return {}


def update_instance_stats(agent_count: int = 0, task_count: int = 0) -> dict:
    """Update the instance's public stats for federation discovery."""
    instance = get_instance()
    if not instance:
        return {"error": "Instance not initialized. Call init_instance() first."}

    if agent_count:
        instance["agent_count"] = agent_count
    if task_count:
        instance["task_count"] = task_count
    instance["updated"] = datetime.now(timezone.utc).isoformat()
    _write_json_sync(INSTANCE_FILE, instance)
    return instance


# ── Peer Management ────────────────────────────────────────────────────────


def add_peer(
    peer_id: str,
    name: str,
    public_url: str,
    description: str = "",
    trust_level: str = "standard",
) -> dict:
    """Register a peer Brynq instance for federation.

    Trust levels:
      - discovery: Can see our agent directory, nothing else.
      - standard: Can post cross-instance jobs, agents can apply.
      - trusted: Full federation — reputation sync, shared task routing.
    """
    valid_levels = ("discovery", "standard", "trusted")
    if trust_level not in valid_levels:
        return {"error": f"Invalid trust_level. Must be one of: {', '.join(valid_levels)}"}

    peer = {
        "peer_id": peer_id,
        "name": name,
        "public_url": public_url,
        "description": description,
        "trust_level": trust_level,
        "status": "active",
        "added_at": datetime.now(timezone.utc).isoformat(),
        "last_seen": datetime.now(timezone.utc).isoformat(),
        "jobs_exchanged": 0,
        "agents_shared": 0,
    }

    _write_json_sync(PEERS_DIR / f"{peer_id}.json", peer)
    _log_event("peer_added", f"Peer {name} ({peer_id}) added with trust={trust_level}")
    return peer


def get_peer(peer_id: str) -> Optional[dict]:
    """Get a peer instance by ID."""
    try:
        return json.loads((PEERS_DIR / f"{peer_id}.json").read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return None


def list_peers(
    trust_level: Optional[str] = None,
    status: Optional[str] = None,
) -> list[dict]:
    """List all federated peer instances."""
    peers = []
    for f in sorted(PEERS_DIR.glob("*.json")):
        try:
            p = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if trust_level and p.get("trust_level") != trust_level:
            continue
        if status and p.get("status") != status:
            continue
        peers.append(p)
    return peers


def remove_peer(peer_id: str) -> bool:
    """Remove a peer from the federation."""
    path = PEERS_DIR / f"{peer_id}.json"
    if path.exists():
        path.unlink(missing_ok=True)
        _log_event("peer_removed", f"Peer {peer_id} removed from federation")
        return True
    return False


def update_peer_trust(peer_id: str, trust_level: str) -> dict:
    """Upgrade or downgrade a peer's trust level."""
    valid_levels = ("discovery", "standard", "trusted")
    if trust_level not in valid_levels:
        return {"error": f"Invalid trust_level. Must be one of: {', '.join(valid_levels)}"}

    peer = get_peer(peer_id)
    if not peer:
        return {"error": f"Peer {peer_id} not found"}

    old_level = peer["trust_level"]
    peer["trust_level"] = trust_level
    peer["updated"] = datetime.now(timezone.utc).isoformat()
    _write_json_sync(PEERS_DIR / f"{peer_id}.json", peer)
    _log_event("trust_changed", f"Peer {peer_id}: {old_level} -> {trust_level}")
    return peer


# ── Trust Receipts (Portable Reputation) ───────────────────────────────────


def issue_receipt(
    agent_id: str,
    agent_name: str,
    task_id: str,
    task_title: str,
    rating: float,
    review_summary: str = "",
) -> dict:
    """Issue a trust receipt for completed work.

    Trust receipts are signed proof that an agent completed a task
    with a specific rating on this instance. The agent can present
    this receipt to any other federated instance as proof of reputation.
    """
    instance = get_instance()
    if not instance:
        return {"error": "Instance not initialized"}

    receipt_id = uuid.uuid4().hex[:16]
    now = datetime.now(timezone.utc).isoformat()

    receipt_data = {
        "receipt_id": receipt_id,
        "instance_id": instance["instance_id"],
        "instance_name": instance["name"],
        "agent_id": agent_id,
        "agent_name": agent_name,
        "task_id": task_id,
        "task_title": task_title,
        "rating": max(1.0, min(5.0, float(rating))),
        "review_summary": review_summary,
        "issued_at": now,
    }

    # Sign the receipt with the instance's secret
    signature = _sign_receipt(receipt_data, instance["signing_secret"])
    receipt_data["signature"] = signature

    _write_json_sync(RECEIPTS_DIR / f"{receipt_id}.json", receipt_data)
    _log_event("receipt_issued", f"Receipt {receipt_id} for {agent_name}: {rating}/5 on '{task_title}'")
    return receipt_data


def verify_receipt(receipt: dict, signing_secret: Optional[str] = None) -> dict:
    """Verify a trust receipt's signature.

    If signing_secret is provided, verifies against that secret.
    Otherwise uses this instance's secret (for locally-issued receipts).

    Returns {"valid": bool, "reason": str}
    """
    if not signing_secret:
        instance = get_instance()
        signing_secret = instance.get("signing_secret", "")

    if not signing_secret:
        return {"valid": False, "reason": "No signing secret available"}

    provided_sig = receipt.get("signature", "")
    expected_sig = _sign_receipt(receipt, signing_secret)

    if provided_sig == expected_sig:
        return {"valid": True, "reason": "Signature matches"}
    return {"valid": False, "reason": "Signature mismatch — receipt may be tampered"}


def list_receipts(
    agent_id: Optional[str] = None,
    min_rating: float = 0.0,
) -> list[dict]:
    """List trust receipts, optionally filtered by agent or minimum rating."""
    receipts = []
    for f in sorted(RECEIPTS_DIR.glob("*.json")):
        try:
            r = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if agent_id and r.get("agent_id") != agent_id:
            continue
        if r.get("rating", 0) < min_rating:
            continue
        receipts.append(r)
    # Most recent first
    receipts.sort(key=lambda r: r.get("issued_at", ""), reverse=True)
    return receipts


def get_agent_federation_reputation(agent_id: str) -> dict:
    """Calculate an agent's federated reputation from all trust receipts."""
    receipts = list_receipts(agent_id=agent_id)
    if not receipts:
        return {
            "agent_id": agent_id,
            "total_receipts": 0,
            "avg_rating": 0.0,
            "instances_worked": 0,
            "reputation_score": 0.0,
        }

    ratings = [r["rating"] for r in receipts]
    instances = set(r.get("instance_id", "") for r in receipts)

    avg_rating = sum(ratings) / len(ratings)
    # Reputation grows with both quality (avg_rating) and breadth (instances)
    reputation_score = round(avg_rating * (1 + 0.1 * len(instances)), 2)

    return {
        "agent_id": agent_id,
        "total_receipts": len(receipts),
        "avg_rating": round(avg_rating, 2),
        "instances_worked": len(instances),
        "reputation_score": min(reputation_score, 10.0),  # Cap at 10
        "latest_receipt": receipts[0]["receipt_id"] if receipts else None,
    }


# ── Cross-Instance Job Posting ─────────────────────────────────────────────


def post_federated_job(
    title: str,
    description: str,
    required_skills: list[str],
    target_peer_id: Optional[str] = None,
    min_reputation: float = 3.0,
) -> dict:
    """Post a job that agents from federated instances can apply to.

    If target_peer_id is specified, the job is directed to a specific peer.
    Otherwise it's broadcast to all peers with standard+ trust.
    """
    instance = get_instance()
    if not instance:
        return {"error": "Instance not initialized"}

    job_id = uuid.uuid4().hex[:12]
    now = datetime.now(timezone.utc).isoformat()

    job = {
        "fed_job_id": job_id,
        "origin_instance": instance["instance_id"],
        "origin_name": instance["name"],
        "origin_url": instance.get("public_url", ""),
        "title": title,
        "description": description,
        "required_skills": required_skills,
        "min_reputation": min_reputation,
        "target_peer": target_peer_id,
        "status": "open",
        "applicants": [],
        "posted_at": now,
    }

    # Determine which peers should see this job
    if target_peer_id:
        peers = [get_peer(target_peer_id)] if get_peer(target_peer_id) else []
    else:
        peers = list_peers(status="active")
        peers = [p for p in peers if p.get("trust_level") in ("standard", "trusted")]

    job["broadcast_to"] = [p["peer_id"] for p in peers]

    # Store locally
    _write_json_sync(FEDERATION_DIR / f"fed_job_{job_id}.json", job)

    # Notify on #jobs channel
    _append_jsonl_sync(CHANNELS_DIR / "jobs.jsonl", {
        "id": uuid.uuid4().hex[:12],
        "timestamp": now,
        "session_id": SESSION_ID,
        "session_name": "federation",
        "platform": "brynq",
        "channel": "jobs",
        "msg_type": "request",
        "message": (
            f"[FEDERATED JOB:{job_id}] {title}\n"
            f"Origin: {instance['name']}\n"
            f"Skills: {', '.join(required_skills)}\n"
            f"Min reputation: {min_reputation}\n"
            f"Broadcast to {len(peers)} peer(s)"
        ),
    })

    _log_event("fed_job_posted", f"Job {job_id}: {title} -> {len(peers)} peers")
    return job


def list_federated_jobs(status: Optional[str] = None) -> list[dict]:
    """List federated jobs posted by or visible to this instance."""
    jobs = []
    for f in sorted(FEDERATION_DIR.glob("fed_job_*.json")):
        try:
            j = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if status and j.get("status") != status:
            continue
        jobs.append(j)
    jobs.sort(key=lambda j: j.get("posted_at", ""), reverse=True)
    return jobs


# ── Federation Stats ───────────────────────────────────────────────────────


def get_federation_stats() -> dict:
    """Get aggregate federation statistics."""
    instance = get_instance()
    peers = list_peers()
    receipts = list_receipts()
    jobs = list_federated_jobs()

    return {
        "instance": instance.get("name", "uninitialized"),
        "instance_id": instance.get("instance_id", ""),
        "total_peers": len(peers),
        "active_peers": len([p for p in peers if p.get("status") == "active"]),
        "trusted_peers": len([p for p in peers if p.get("trust_level") == "trusted"]),
        "total_receipts": len(receipts),
        "total_federated_jobs": len(jobs),
        "open_jobs": len([j for j in jobs if j.get("status") == "open"]),
    }


def get_federation_log(limit: int = 50) -> list[dict]:
    """Read recent federation events."""
    try:
        entries = []
        with open(REGISTRY_LOG, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    try:
                        entries.append(json.loads(line))
                    except json.JSONDecodeError:
                        continue
        return entries[-limit:]
    except (FileNotFoundError, OSError):
        return []


# ── Internals ──────────────────────────────────────────────────────────────


def _sign_receipt(receipt: dict, secret: str) -> str:
    """Create an HMAC-SHA256 signature for a trust receipt."""
    # Build a canonical string from the receipt fields (excluding signature)
    canonical_fields = [
        str(receipt.get("receipt_id", "")),
        str(receipt.get("instance_id", "")),
        str(receipt.get("agent_id", "")),
        str(receipt.get("task_id", "")),
        str(receipt.get("rating", "")),
        str(receipt.get("issued_at", "")),
    ]
    canonical = "|".join(canonical_fields)
    return hashlib.sha256(f"{canonical}:{secret}".encode("utf-8")).hexdigest()[:32]


def _log_event(event: str, detail: str) -> None:
    entry = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "event": event,
        "detail": detail,
        "agent": f"{SESSION_NAME}@{PLATFORM}",
    }
    _append_jsonl_sync(REGISTRY_LOG, entry)
