import argparse
import json
import re
from pathlib import Path


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


def _safe_repo_id(origin_repo):
    value = str(origin_repo or "unknown-repo").strip()
    value = re.sub(r"[^A-Za-z0-9_.-]+", "-", value)
    return value.strip("-") or "unknown-repo"


def import_submission(package_path, inbox_dir):
    package = _read_json(package_path)
    repo_id = _safe_repo_id(package.get("origin_repo"))
    request_id = _safe_repo_id(package.get("source_request_id") or package.get("id") or "request")
    destination = Path(inbox_dir) / repo_id / "incoming" / f"{request_id}.json"
    destination.parent.mkdir(parents=True, exist_ok=True)
    destination.write_text(json.dumps(package, indent=2) + "\n", encoding="utf-8")
    return destination


def main():
    parser = argparse.ArgumentParser(description="Stage an upstream request submission in the hub inbox.")
    parser.add_argument("--package", required=True)
    parser.add_argument("--inbox", default=".prd_plugin/inbox")
    args = parser.parse_args()

    destination = import_submission(args.package, args.inbox)
    print(str(destination))


if __name__ == "__main__":
    main()


INBOX_DIR = ".prd_plugin/inbox"
REQUESTS_FILE = ".prd_plugin/state/requests.json"


def _load(path):
    import json as _json
    return _json.loads(Path(path).read_text(encoding="utf-8-sig"))


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


# A closed request is not reopened by re-reading history (REQ-134).
TERMINAL_STATUSES = {"implemented", "rejected", "deferred"}


def _next_local_message_id(local_ids):
    """Allocate a message id from OUR sequence, never reusing a peer's."""
    highest = 0
    for value in local_ids:
        text = str(value or "")
        if text.startswith("MSG-"):
            digits = text[4:].split("-")[0]
            if digits.isdigit():
                highest = max(highest, int(digits))
    return f"MSG-{highest + 1:03d}"


def reconcile_inbox(repo_root=".", requests_path=None):
    """Merge inbox package UPDATES into already-imported canonical requests.

    Import only ever handled NEW packages (REQ-134). When a downstream repo
    answered a question, its refreshed package landed in the inbox and nothing
    merged the new messages into canonical state — the answer sat unread while
    message_check reported the package as "imported", because a match existed.

    Matching is by origin_repo + source_request_id, the same provenance the
    import records. Messages are merged by id and body so re-running changes
    nothing. A package with no canonical match is NEW intake and is reported,
    never silently merged.
    """
    root = Path(repo_root)
    target = Path(requests_path) if requests_path else root / REQUESTS_FILE
    report = {"reconciled": 0, "revised": 0, "updated_ids": [], "unmatched": [],
              "packages": []}
    inbox = root / INBOX_DIR
    if not target.is_file() or not inbox.is_dir():
        return report

    data = _load(target)
    requests = data.get("requests", [])
    by_provenance = {}
    for request in requests:
        if not isinstance(request, dict):
            continue
        origin = request.get("origin_repo")
        source = request.get("source_request_id")
        if origin and source:
            by_provenance[(origin, source)] = request

    changed = False
    for path in sorted(inbox.rglob("*.json")):
        try:
            package = _load(path)
        except (OSError, ValueError):
            continue
        if not isinstance(package, dict):
            continue
        origin = (package.get("origin_repo")
                  or (path.parent.parent.name if path.parent.name == "incoming"
                      else path.parent.name))
        source = package.get("source_request_id") or package.get("id")
        report["packages"].append(str(path.relative_to(root).as_posix()))
        local = by_provenance.get((origin, source))
        if local is None:
            report["unmatched"].append(package.get("id", "<missing-id>"))
            continue

        thread = local.setdefault("thread", {})
        messages = thread.setdefault("messages", [])

        # Message ids are allocated PER REPO, so a downstream MSG-005 and our
        # MSG-005 are different messages that collide, while a message they
        # EDITED keeps its id and would look new. Neither local id nor body is
        # a usable identity. Provenance is: an inbound message is identified by
        # (origin_repo, their message id) for the life of the thread, and gets
        # a fresh local id from our own sequence so it can never collide.
        by_source = {}
        for existing in messages:
            if isinstance(existing, dict) and existing.get("source_message_id"):
                key = (existing.get("origin_repo"), existing.get("source_message_id"))
                by_source[key] = existing
        local_ids = {m.get("id") for m in messages if isinstance(m, dict)}

        added = 0
        revised = 0
        for message in (package.get("thread") or {}).get("messages", []):
            if not isinstance(message, dict):
                continue
            source_id = message.get("id")
            key = (origin, source_id)
            body = message.get("body")

            existing = by_source.get(key)
            if existing is None:
                # Messages merged before provenance stamping (or imported by an
                # older plugin) carry no source id. Adopt one that matches by
                # body instead of appending a duplicate — without this the
                # first run after upgrading would double every inbound message.
                for candidate in messages:
                    if (isinstance(candidate, dict)
                            and not candidate.get("source_message_id")
                            and candidate.get("body") == body):
                        candidate["origin_repo"] = origin
                        candidate["source_message_id"] = source_id
                        by_source[key] = candidate
                        existing = candidate
                        changed = True
                        break
            if existing is not None:
                # Same message seen before. Identical body -> nothing to do
                # (this is what makes re-running idempotent). Different body ->
                # they revised it upstream; converge on their text rather than
                # appending a near-identical twin, and say so in the report.
                if existing.get("body") != body:
                    existing["body"] = body
                    existing["revised"] = True
                    revised += 1
                    changed = True
                continue

            incoming = dict(message)
            incoming["origin_repo"] = origin
            incoming["source_message_id"] = source_id
            incoming["id"] = _next_local_message_id(local_ids)
            # Inbound messages have already travelled; they are not ours to send.
            incoming.setdefault("delivery", {"state": "received"})
            messages.append(incoming)
            by_source[key] = incoming
            local_ids.add(incoming["id"])
            added += 1

        if added:
            # The asked party answering clears our blocked state — but a request
            # we already closed is not reopened by re-reading its history. A
            # terminal request keeps its resolution; the messages still merge
            # (nothing is lost) and the merge is reported so it stays visible.
            if local.get("status") == "needs_info":
                local["status"] = "in_review"
            if local.get("status") not in TERMINAL_STATUSES:
                thread["status"] = "open"
            report["reconciled"] += added
            report["updated_ids"].append(local.get("id"))
            changed = True
        if revised:
            report["revised"] += revised
            if local.get("id") not in report["updated_ids"]:
                report["updated_ids"].append(local.get("id"))

    if changed:
        _store(target, data)
    return report
