"""Address a request to a peer repo (REQ-136).

Deliberately small. A request already travels to the configured upstream hub;
this adds the only missing piece — naming a peer and dropping the package in
its inbox — without a second directory, a handshake, or anything new that can
block a message.

The rules it enforces, decided in DEC-011 and DEC-012:

- Filing is local and ALWAYS succeeds. No routing decision may destroy what an
  agent wrote.
- A peer is declared once as `requests.peers` {repo id -> path}. A configured
  path IS the consent to deliver there, exactly as `requests.upstream_hub_path`
  already works.
- Declaration is one-sided. An inbox is a mailbox, not canonical state: import
  stays the recipient's explicit act, so an unsolicited package cannot corrupt
  their truth.
- An UNDECLARED target refuses delivery only, and names the fix — writing into
  a repo you were not pointed at is the consent floor.
- An UNREACHABLE declared target holds with a named finding and retries on the
  next flush, exactly as replies already do.
- Messaging a repo in your own workspace warns and proceeds. It crosses no
  boundary, so refusing it would buy no security while blocking the legitimate
  case of wanting a durable record instead of ephemeral chat.
"""

import argparse
import json
import sys
from pathlib import Path

CONFIG_FILE = ".prd_plugin/config.json"
SERVICES_FILE = ".prd_plugin/services.json"
REQUESTS_FILE = ".prd_plugin/state/requests.json"

# A manifest that has not been filled in says id="auto" or workspace="". Those
# are absent values, not shared ones: treating blank as equal would fire the
# workspace advisory on every send.
PLACEHOLDER_IDENTITY = {"", "auto", "unknown", "unknown-repo", None}

# A closed request does not travel. Mirrors request_import's set.
TERMINAL_STATUSES = {"implemented", "rejected", "deferred"}


def _read_json(path, default=None):
    try:
        return json.loads(Path(path).read_text(encoding="utf-8-sig"))
    except (OSError, ValueError):
        return {} if default is None else default


def _write_json(path, data):
    Path(path).write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n",
                          encoding="utf-8", newline="\n")


def _safe_repo_id(value):
    text = str(value or "unknown-repo").strip()
    keep = [c if (c.isalnum() or c in "-_.") else "-" for c in text]
    return "".join(keep).strip("-") or "unknown-repo"


def declared_peers(repo_root):
    """The {repo id -> path} map. One place, no second registry."""
    config = _read_json(Path(repo_root) / CONFIG_FILE)
    peers = ((config.get("requests") or {}).get("peers")) or {}
    return {str(k): str(v) for k, v in peers.items() if isinstance(peers, dict)}


def repository_identity(repo_root):
    manifest = _read_json(Path(repo_root) / SERVICES_FILE)
    repository = manifest.get("repository") or {}
    return {"id": repository.get("id") or "", "workspace": repository.get("workspace") or ""}


def _is_prd_repo(path):
    return (Path(path) / ".prd_plugin").is_dir()


def list_destinations(repo_root):
    """Who can this repo message, and is each one actually reachable?"""
    root = Path(repo_root)
    identity = repository_identity(root)
    destinations = []
    for repo_id, raw_path in sorted(declared_peers(root).items()):
        path = Path(raw_path)
        reachable = _is_prd_repo(path)
        reason = ""
        if not path.exists():
            reason = f"no directory at {raw_path}"
        elif not reachable:
            reason = f"{raw_path} exists but has no .prd_plugin directory"
        peer_identity = repository_identity(path) if reachable else {"id": "", "workspace": ""}
        destinations.append({
            "repo_id": repo_id,
            "path": raw_path,
            "reachable": reachable,
            "reason": reason,
            "same_workspace": _same_workspace(identity, peer_identity),
        })
    return {"repo_id": identity["id"], "workspace": identity["workspace"],
            "destinations": destinations}


def _same_workspace(left, right):
    a = str(left.get("workspace") or "").strip()
    b = str(right.get("workspace") or "").strip()
    if a in PLACEHOLDER_IDENTITY or b in PLACEHOLDER_IDENTITY:
        return False
    return a == b


def _build_package(request, origin_repo):
    """The package the peer imports.

    Uses the exporter's PEER builder, not the upstream one. The two directions
    carry different visibility sets and swapping them silently strips message
    bodies — the failure that delivered nine empty replies (REQ-131).
    """
    sys.path.insert(0, str(Path(__file__).resolve().parent))
    import request_export
    package = request_export.build_peer_package(request, origin_repo)
    package.setdefault("schema_version", "0.1")
    return package


def deliver_request(repo_root, request_id, requests_path=None):
    """Drop an addressed request into the target repo's inbox and verify it.

    Returns a result dict rather than raising for routing outcomes: an
    undelivered request is a reportable state, not a crash, because the request
    itself is never at risk.
    """
    root = Path(repo_root)
    target_file = Path(requests_path) if requests_path else root / REQUESTS_FILE
    data = _read_json(target_file, {"requests": []})
    request = next((r for r in data.get("requests", [])
                    if isinstance(r, dict) and r.get("id") == request_id), None)
    if request is None:
        return {"delivered": False, "state": "unknown_request", "id": request_id,
                "reason": f"{request_id} is not in {target_file}"}

    target_repo = request.get("target_repo")
    if not target_repo:
        return {"delivered": False, "state": "not_addressed", "id": request_id,
                "reason": f"{request_id} has no target_repo; it is a local request"}

    identity_now = repository_identity(root)
    our_id = identity_now["id"] or _safe_repo_id(root.resolve().name)

    # `target_repo` predates this feature: it records who a request is FOR, and
    # every IMPORTED request already carries it pointing at whoever received
    # it. Sending on that field alone mailed a closed request back to its own
    # origin. Only requests this repo authored are ours to send.
    origin = request.get("origin_repo")
    if origin and origin != our_id:
        return {"delivered": False, "state": "not_ours_to_send", "id": request_id,
                "target_repo": target_repo,
                "reason": (f"{request_id} was imported from {origin!r}; its target_repo records who "
                           f"it was addressed to, not somewhere for us to send it. Reply on its "
                           f"thread instead.")}

    if str(target_repo) == our_id:
        return {"delivered": False, "state": "self_addressed", "id": request_id,
                "target_repo": target_repo,
                "reason": f"{request_id} is addressed to this repo ({our_id!r}); it is already here."}

    if request.get("status") in TERMINAL_STATUSES:
        return {"delivered": False, "state": "terminal", "id": request_id,
                "target_repo": target_repo,
                "reason": (f"{request_id} is {request.get('status')!r}; a closed request is not "
                           f"delivered. Reopen it or file a new one if it still needs to travel.")}

    peers = declared_peers(root)
    if target_repo not in peers:
        return {"delivered": False, "state": "undeclared", "id": request_id,
                "target_repo": target_repo,
                "reason": (f"{target_repo!r} is not a declared peer, so nothing was written to it. "
                           f"Declare it in requests.peers as {target_repo!r} -> <path to that repo> "
                           f"and deliver again. The request is unchanged.")}

    peer_root = Path(peers[target_repo])
    if not _is_prd_repo(peer_root):
        return {"delivered": False, "state": "unreachable", "id": request_id,
                "target_repo": target_repo, "path": str(peer_root),
                "reason": (f"{target_repo!r} is declared at {peer_root} but no PRD Plugin repo is "
                           f"there right now, so the request is held and will be retried on the "
                           f"next flush.")}

    identity = repository_identity(root)
    origin_repo = identity["id"] or _safe_repo_id(root.resolve().name)
    advisories = []
    if _same_workspace(identity, repository_identity(peer_root)):
        advisories.append(
            f"{target_repo!r} is in your own workspace ({identity['workspace']!r}); its agents "
            f"share your chat. Delivered anyway — use this when you want a durable auditable "
            f"request rather than ephemeral coordination.")

    package = _build_package(request, origin_repo)
    destination = (peer_root / ".prd_plugin" / "inbox" / _safe_repo_id(origin_repo)
                   / "incoming" / f"{_safe_repo_id(request_id)}.json")
    destination.parent.mkdir(parents=True, exist_ok=True)
    _write_json(destination, package)

    # Read the written artifact back: the peer's inbox is the authority on
    # whether this arrived, not our intent to send it.
    landed = _read_json(destination)
    if landed.get("source_request_id") != request_id:
        return {"delivered": False, "state": "verify_failed", "id": request_id,
                "target_repo": target_repo, "destination": str(destination),
                "reason": (f"wrote {destination} but reading it back did not return "
                           f"{request_id}; treating the delivery as failed")}

    # Stamp the delivery so a later flush does not rewrite the package and
    # resurrect a finished conversation. Same contract as replies (REQ-133):
    # delivered is only recorded after the read-back above succeeded.
    request["delivery"] = {"state": "delivered", "target_repo": target_repo,
                           "destination": str(destination)}
    _write_json(target_file, data)

    return {"delivered": True, "state": "delivered", "id": request_id,
            "target_repo": target_repo, "destination": str(destination),
            "advisories": advisories}


def flush_addressed_requests(repo_root, requests_path=None):
    """Deliver every addressed request, and report the ones that could not go.

    Nothing is retried destructively and nothing is dropped: a request that
    cannot be delivered stays exactly where it is and appears in `undelivered`
    with the reason, so a silent black hole is impossible.
    """
    root = Path(repo_root)
    target_file = Path(requests_path) if requests_path else root / REQUESTS_FILE
    data = _read_json(target_file, {"requests": []})
    report = {"delivered": 0, "destinations": [], "undelivered": [], "advisories": []}
    for request in data.get("requests", []):
        if not isinstance(request, dict) or not request.get("target_repo"):
            continue
        if (request.get("delivery") or {}).get("state") == "delivered":
            continue
        result = deliver_request(root, request.get("id"), requests_path=target_file)
        if result.get("delivered"):
            report["delivered"] += 1
            report["destinations"].append(result["destination"])
            report["advisories"].extend(result.get("advisories", []))
        else:
            report["undelivered"].append({
                "id": result.get("id"), "state": result.get("state"),
                "target_repo": result.get("target_repo"), "reason": result.get("reason")})
    return report


def main():
    parser = argparse.ArgumentParser(description="Address and deliver requests to peer repos.")
    parser.add_argument("action", choices=["destinations", "deliver", "flush"])
    parser.add_argument("--repo-root", default=".")
    parser.add_argument("--request-id", default=None)
    args = parser.parse_args()

    if args.action == "destinations":
        result = list_destinations(args.repo_root)
    elif args.action == "deliver":
        if not args.request_id:
            parser.error("--request-id is required for deliver")
        result = deliver_request(args.repo_root, args.request_id)
    else:
        result = flush_addressed_requests(args.repo_root)
    print(json.dumps(result, indent=2, ensure_ascii=False))
    if args.action == "deliver" and not result.get("delivered"):
        return 2
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
