import argparse
import hashlib
import json
import sys
import re
from pathlib import Path


def _read_json(path):
    return json.loads(Path(path).read_text(encoding="utf-8-sig"))


def _write_json(path, data):
    Path(path).parent.mkdir(parents=True, exist_ok=True)
    Path(path).write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")


def _next_message_id(request):
    max_id = 0
    for message in request.get("thread", {}).get("messages", []):
        match = re.match(r"MSG-(\d+)$", str(message.get("id", "")))
        if match:
            max_id = max(max_id, int(match.group(1)))
    return f"MSG-{max_id + 1:03d}"


def append_reply(
    requests_path,
    request_id,
    body,
    author_agent,
    author_session,
    visibility="repo",
    reply_to=None,
):
    data = _read_json(requests_path)
    requests = data.get("requests", [])
    for request in requests:
        if request.get("id") != request_id:
            continue
        thread = request.setdefault("thread", {})
        messages = thread.setdefault("messages", [])
        # Writing a reply and SENDING it were separate operations, so replies
        # sat unsent indefinitely with nothing saying so (REQ-133). Every
        # message now carries its delivery state from birth: outbound replies
        # start `pending` and only a verified send marks them `delivered`.
        outbound = str(visibility).strip().lower() in OUTBOUND_VISIBILITIES
        message = {
            "id": _next_message_id(request),
            "created_at": "",
            "author_agent": author_agent,
            "author_session": author_session,
            "visibility": visibility,
            "reply_to": reply_to,
            "body": body,
            "source_refs": [],
            "delivery": {"state": "pending" if outbound else "not_outbound"},
        }
        messages.append(message)
        thread["status"] = "open"
        # Only the ASKED party answering clears needs_info. Visibility is
        # directional: `upstream` travels to the hub, so it is the reply that
        # answers a hub question. An outbound reply (repo/public) is us
        # speaking, and must not downgrade our own blocked state (REQ-131).
        if request.get("status") == "needs_info" and visibility == "upstream":
            request["status"] = "in_review"
        _write_json(requests_path, data)
        return message
    raise ValueError(f"request {request_id} was not found")


# A reply the ORIGIN REPO can receive. `upstream` is hub-only and never travels
# outward, so it is not an unsent reply.
OUTBOUND_VISIBILITIES = {"repo", "public"}


class DeliveryError(RuntimeError):
    """A reply was appended locally but did not land intact upstream."""


def _body_digest(body):
    return hashlib.sha256(body.encode("utf-8")).hexdigest()


def deliver_reply(repo_root, request_id, body, author_agent="AGENT-001",
                  author_session="SES-001", visibility="upstream",
                  reply_to=None, hub_root=None, repo_id=None):
    """Append a reply AND deliver it upstream, verifying the body landed.

    One deterministic operation (REQ-132, from ai-collab-v3 REQ-140): append
    locally, regenerate the sanitized package, write it into the destination
    inbox, then READ IT BACK and confirm the exact message id and body digest
    survived. Two silent failures made this necessary:

    - the exporter drops a whole message that references local state, so a
      reply could vanish with the caller told nothing; and
    - autosubmit skips an already-submitted request, so later replies never
      travelled at all.

    Never reports success from intent: if sanitization drops or alters the
    reply, it raises and names the rejecting rule.
    """
    repo_root = Path(repo_root)
    requests_path = repo_root / ".prd_plugin" / "state" / "requests.json"
    message = append_reply(requests_path, request_id, body, author_agent,
                           author_session, visibility=visibility, reply_to=reply_to)
    digest = _body_digest(body)

    sys.path.insert(0, str(Path(__file__).resolve().parent))
    import request_export

    data = _read_json(requests_path)
    request = next((r for r in data.get("requests", []) if r.get("id") == request_id), None)
    if request is None:
        raise DeliveryError(f"{request_id} vanished from local state during delivery")

    submission = request_export.build_upstream_submission(request, "prd-plugin")
    survived = [m for m in submission.get("thread", {}).get("messages", [])
                if m.get("id") == message["id"]]
    if not survived:
        raise DeliveryError(
            f"reply {message['id']} was appended locally but sanitization REJECTED it, so it "
            f"cannot be delivered. Cause: the exporter drops messages whose visibility is not "
            f"upstream-visible ({visibility!r} was used), or that reference local runtime "
            f"paths (.prd_plugin/local, absolute drive paths, /Users/, /home/). Rewrite the body "
            f"without the local reference and resend; NOTHING was delivered.")
    if survived[0].get("body") != body:
        raise DeliveryError(
            f"reply {message['id']} was ALTERED by sanitization before delivery; the delivered "
            f"body does not match what was written. Nothing is reported as delivered.")

    if hub_root is None:
        raise DeliveryError("no upstream hub configured; the reply is local-only and undelivered")
    repo_id = repo_id or repo_root.resolve().name
    destination = (Path(hub_root) / ".prd_plugin" / "inbox" / repo_id / "incoming"
                   / f"{request_id}.json")
    destination.parent.mkdir(parents=True, exist_ok=True)
    destination.write_text(json.dumps(submission, indent=2, ensure_ascii=False) + "\n",
                           encoding="utf-8")

    # Read back: the delivered artifact is the authority, not our intent.
    landed = _read_json(destination)
    delivered = next((m for m in landed.get("thread", {}).get("messages", [])
                      if m.get("id") == message["id"]), None)
    if delivered is None:
        raise DeliveryError(f"read-back failed: {message['id']} is absent from {destination}")
    verified = _body_digest(delivered.get("body", ""))
    if verified != digest:
        raise DeliveryError(
            f"read-back digest mismatch for {message['id']}: delivered body does not match "
            f"the reply that was written")
    return {"delivered": True, "message_id": message["id"], "body_sha256": digest,
            "verified_body_sha256": verified, "destination": str(destination),
            "request_id": request_id}


def _mark_delivered(requests_path, request_id, message_ids, destination):
    data = _read_json(requests_path)
    for request in data.get("requests", []):
        if request.get("id") != request_id:
            continue
        for message in (request.get("thread") or {}).get("messages", []):
            if message.get("id") in message_ids:
                message["delivery"] = {
                    "state": "delivered",
                    "destination": str(destination),
                    "body_sha256": _body_digest(message.get("body", "")),
                }
    _write_json(requests_path, data)


def flush_pending_replies(repo_root, peers=None, requests_path=None):
    """Send every reply still marked pending, then verify it landed (REQ-133).

    This is the mechanism that makes an unsent reply impossible to leave
    behind: the hub's send IS the scoped mailbox publish, so flushing
    republishes each origin repo's mailbox and only marks messages delivered
    after reading the published artifact back. A destination that cannot be
    reached leaves the reply PENDING and is reported — never silently cleared.

    `peers` maps origin repo id -> repo root. Omit it to resolve peers as
    sibling directories of this repo.
    """
    root = Path(repo_root)
    target = Path(requests_path) if requests_path else root / ".prd_plugin" / "state" / "requests.json"
    report = {"delivered": 0, "mailboxes_published": [], "undeliverable": [], "pending": []}
    if not target.is_file():
        return report

    data = _read_json(target)
    by_origin = {}
    for request in data.get("requests", []):
        if not isinstance(request, dict):
            continue
        origin = request.get("origin_repo")
        if not origin:
            continue
        pending = [m.get("id") for m in (request.get("thread") or {}).get("messages", [])
                   if isinstance(m, dict)
                   and (m.get("delivery") or {}).get("state") == "pending"]
        if pending:
            by_origin.setdefault(origin, []).append((request.get("id"), pending))
    if not by_origin:
        return report

    sys.path.insert(0, str(Path(__file__).resolve().parent))
    import request_mailbox

    for origin, rows in sorted(by_origin.items()):
        peer_root = None
        if peers and origin in peers:
            peer_root = Path(peers[origin])
        else:
            candidate = root.resolve().parent / origin
            if (candidate / ".prd_plugin").is_dir():
                peer_root = candidate
        if peer_root is None or not peer_root.exists():
            for request_id, message_ids in rows:
                report["undeliverable"].append({
                    "id": request_id, "origin_repo": origin,
                    "messages": message_ids,
                    "reason": f"no reachable repo for origin {origin!r}; reply stays pending",
                })
                report["pending"].extend(message_ids)
            continue

        destination = peer_root / ".prd_plugin" / "mailboxes" / "prd-plugin" / "mailbox.json"
        destination.parent.mkdir(parents=True, exist_ok=True)
        mailbox = request_mailbox.build_mailbox(origin, data.get("requests", []))
        destination.write_text(json.dumps(mailbox, indent=2, ensure_ascii=False) + "\n",
                               encoding="utf-8")

        # Read the published artifact back before claiming anything was sent.
        landed = _read_json(destination)
        landed_ids = {m.get("id")
                      for r in landed.get("requests", [])
                      for m in (r.get("thread") or {}).get("messages", [])
                      if isinstance(m, dict)}
        report["mailboxes_published"].append(origin)
        for request_id, message_ids in rows:
            arrived = [mid for mid in message_ids if mid in landed_ids]
            missing = [mid for mid in message_ids if mid not in landed_ids]
            if arrived:
                _mark_delivered(target, request_id, set(arrived), destination)
                report["delivered"] += len(arrived)
            if missing:
                report["undeliverable"].append({
                    "id": request_id, "origin_repo": origin, "messages": missing,
                    "reason": ("withheld by mailbox scoping — the reply is not visible to "
                               "the origin repo; use --visibility repo"),
                })
                report["pending"].extend(missing)
    return report


def main():
    parser = argparse.ArgumentParser(description="Append a local MSG-* reply to a request.")
    parser.add_argument("--requests", default=".prd_plugin/state/requests.json")
    parser.add_argument("--request-id", required=True)
    parser.add_argument("--body", required=True)
    parser.add_argument("--author-agent", default="AGENT-001")
    parser.add_argument("--author-session", default="SES-001")
    parser.add_argument("--visibility", choices=("repo", "upstream", "public"), default="repo")
    parser.add_argument("--reply-to")
    parser.add_argument("--deliver", action="store_true",
                        help="Append AND deliver upstream, verifying the body landed (REQ-132).")
    parser.add_argument("--repo-root", default=".")
    parser.add_argument("--hub-root", default=None,
                        help="Upstream hub checkout; defaults to PRD_UPSTREAM_HUB or config.")
    parser.add_argument("--repo-id", default=None)
    args = parser.parse_args()

    if args.deliver:
        hub_root = args.hub_root
        if not hub_root:
            import os
            hub_root = (os.environ.get("PRD_UPSTREAM_HUB") or "").strip() or None
        if not hub_root:
            try:
                cfg = _read_json(Path(args.repo_root) / ".prd_plugin" / "config.json")
                hub_root = str(cfg.get("requests", {}).get("upstream_hub_path") or "").strip() or None
            except (OSError, ValueError):
                hub_root = None
        try:
            receipt = deliver_reply(args.repo_root, args.request_id, args.body,
                                    args.author_agent, args.author_session,
                                    visibility=args.visibility, reply_to=args.reply_to,
                                    hub_root=hub_root, repo_id=args.repo_id)
        except DeliveryError as exc:
            print(f"[PRD Plugin] reply NOT delivered: {exc}", file=sys.stderr)
            raise SystemExit(2)
        print(json.dumps(receipt, indent=2))
        return

    message = append_reply(
        args.requests,
        args.request_id,
        args.body,
        args.author_agent,
        args.author_session,
        visibility=args.visibility,
        reply_to=args.reply_to,
    )
    print(json.dumps(message, indent=2))


if __name__ == "__main__":
    main()
