#!/usr/bin/env python3
"""Submit a PRD Plugin request upstream, automatically (REQ-058).

Filing a plugin bug and *submitting* it were separate acts: `prd_file_request`
wrote a local REQ record, and someone then had to remember to run
`request_export.py` and carry the package into the hub. Nobody did. Two real
plugin bugs sat unreported in downstream state for weeks.

This module closes the loop. Given a request id, it:

1. Classifies the record against ``prd_gate.PLUGIN_SURFACE_RE`` — the same
   regex the commit gate warns on, imported rather than copied so the two can
   never disagree.
2. If it is about a plugin-owned surface, flags the local record
   (``upstream_submission``/``scope``) and exports a sanitized package to
   ``.prd_plugin/outbox/`` using ``request_export.build_upstream_submission``.
3. Delivers that package into the hub's inbox, when — and only when — the
   operator has pointed at one via ``PRD_UPSTREAM_HUB`` or
   ``config.requests.upstream_hub_path``, and the target really is a PRD Plugin
   hub checkout. Configuring a hub path is the consent; an unconfigured or
   mistyped path never gets written to.

It never raises and never exits non-zero: a failed submission must degrade to a
package sitting in the outbox (where ``prd_gate``'s ``stranded_outbox`` check
picks it up), never to a failed filing.
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path

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

from prd_gate import PLUGIN_SURFACE_RE, HUB_MARKERS  # noqa: E402  (single source of truth)
from request_export import build_upstream_submission  # noqa: E402


def _now():
    return datetime.now(timezone.utc).strftime("%Y-%m-%d")


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


def _style_of(path):
    """Indent, newline, and BOM of an existing JSON file, so rewriting it stays a
    minimal diff. Mirrors the invariant mcp/server.cjs holds for the same files."""
    try:
        raw = Path(path).read_bytes()
    except OSError:
        return 2, "\n", False
    bom = raw.startswith(b"\xef\xbb\xbf")
    text = raw[3:].decode("utf-8", "replace") if bom else raw.decode("utf-8", "replace")
    newline = "\r\n" if "\r\n" in text else "\n"
    indent = 2
    match = re.search(r"[\r\n]([ \t]+)\S", text)
    if match:
        found = match.group(1)
        indent = "\t" if found.startswith("\t") else len(found)
    return indent, newline, bom


def _write_json(path, data, style=None):
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    indent, newline, bom = style or (2, "\n", False)
    body = json.dumps(data, indent=indent, ensure_ascii=False) + "\n"
    if newline != "\n":
        body = body.replace("\n", newline)
    encoded = body.encode("utf-8")
    path.write_bytes((b"\xef\xbb\xbf" if bom else b"") + encoded)


def _strings(value):
    if isinstance(value, str):
        yield value
    elif isinstance(value, dict):
        for item in value.values():
            yield from _strings(item)
    elif isinstance(value, (list, tuple)):
        for item in value:
            yield from _strings(item)


def is_plugin_surface(record):
    """True when the request is about PRD Plugin itself, not the host project."""
    text = " ".join(_strings(record))
    return bool(PLUGIN_SURFACE_RE.search(text))


def _is_hub(root):
    root = Path(root)
    return all((root / marker).exists() for marker in HUB_MARKERS)


def _skip(reason, **extra):
    return dict({"action": "skipped", "reason": reason, "package": None,
                 "delivered_to": None, "note": ""}, **extra)


def _repo_id_for(root, explicit=None):
    if explicit:
        return explicit
    root = Path(root)
    try:
        cfg = _read_json(root / ".prd_plugin" / "config.json")
        for key in ("repo_id", "project_id"):
            value = cfg.get(key) or cfg.get("plugin", {}).get(key)
            if isinstance(value, str) and value.strip():
                return value.strip()
    except Exception:
        pass
    return root.resolve().name


_SAFE_SEGMENT = re.compile(r"[^A-Za-z0-9._-]+")


def _safe(segment):
    cleaned = _SAFE_SEGMENT.sub("-", str(segment)).strip("-.")
    return cleaned or "unknown-repo"


def resolve_hub(root, env=None):
    """The hub to deliver into, or (None, reason). Configuring one is the consent."""
    env = os.environ if env is None else env
    candidate = (env.get("PRD_UPSTREAM_HUB") or "").strip()
    source = "PRD_UPSTREAM_HUB"
    if not candidate:
        source = "config.requests.upstream_hub_path"
        try:
            cfg = _read_json(Path(root) / ".prd_plugin" / "config.json")
            candidate = str(cfg.get("requests", {}).get("upstream_hub_path") or "").strip()
        except Exception:
            candidate = ""
    if not candidate:
        return None, ("no hub configured — set PRD_UPSTREAM_HUB or "
                      "config.requests.upstream_hub_path to deliver automatically")

    hub = Path(candidate).expanduser()
    if not hub.is_dir():
        return None, f"{source} points at {hub}, which does not exist"
    if not _is_hub(hub):
        return None, f"{source} points at {hub}, which is not a prd-plugin hub checkout"
    return hub, ""


def autosubmit(root, request_id, env=None, repo_id=None):
    """Flag, export, and (when a hub is configured) deliver. Never raises."""
    try:
        return _autosubmit(Path(root), request_id, env, repo_id)
    except Exception as exc:  # pragma: no cover - the fail-open backstop
        return _skip("error", note=f"{type(exc).__name__}: {exc}")


def _autosubmit(root, request_id, env, repo_id):
    if _is_hub(root):
        return _skip("hub_repo", note="every hub request is about the plugin by definition")

    state_path = root / ".prd_plugin" / "state" / "requests.json"
    if not state_path.is_file():
        return _skip("state_unreadable", note=f"{state_path} not found")
    style = _style_of(state_path)
    try:
        state = _read_json(state_path)
        records = state["requests"]
        if not isinstance(records, list):
            raise ValueError("requests must be an array")
    except Exception as exc:
        return _skip("state_unreadable", note=f"{type(exc).__name__}: {exc}")

    record = next((r for r in records if isinstance(r, dict) and r.get("id") == request_id), None)
    if record is None:
        return _skip("request_not_found", note=f"{request_id} is not in {state_path}")
    if record.get("upstream_submission") or record.get("upstream_request_id"):
        return _skip("already_submitted")
    if not is_plugin_surface(record):
        return _skip("not_plugin_surface")

    package = build_upstream_submission(record, "prd-plugin")
    package_path = root / ".prd_plugin" / "outbox" / f"{request_id}-upstream.json"
    _write_json(package_path, package)

    record["upstream_submission"] = True
    record["scope"] = "upstream_submission"
    record["updated_at"] = _now()

    hub, why = resolve_hub(root, env)
    delivered_to = None
    if hub is not None:
        target = (hub / ".prd_plugin" / "inbox" / _safe(_repo_id_for(root, repo_id))
                  / "incoming" / f"{request_id}.json")
        try:
            _write_json(target, package)
            delivered_to = target
            record["submitted_at"] = _now()
            record["submitted_to"] = str(hub)
        except Exception as exc:
            why = f"could not write into {target}: {type(exc).__name__}: {exc}"

    _write_json(state_path, state, style)

    if delivered_to is not None:
        return {"action": "delivered", "reason": "plugin_surface", "package": str(package_path),
                "delivered_to": str(delivered_to), "note": ""}
    return {"action": "exported", "reason": "plugin_surface", "package": str(package_path),
            "delivered_to": None,
            "note": f"{why}. The package is in the outbox; carry it into the hub inbox."}


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--repo-root", default=".")
    parser.add_argument("--request-id", required=True)
    parser.add_argument("--repo-id", help="Inbox folder name in the hub. Defaults to the repo directory name.")
    parser.add_argument("--json", action="store_true", help="Emit the result as JSON.")
    args = parser.parse_args(argv)

    result = autosubmit(args.repo_root, args.request_id, repo_id=args.repo_id)

    if args.json:
        print(json.dumps(result, indent=2))
    elif result["action"] == "delivered":
        print(f"{args.request_id} submitted upstream -> {result['delivered_to']}")
    elif result["action"] == "exported":
        print(f"{args.request_id} exported -> {result['package']}\n  {result['note']}")
    else:
        print(f"{args.request_id}: no submission ({result['reason']})")
    return 0  # never fail a filing


if __name__ == "__main__":
    sys.exit(main())
