#!/usr/bin/env python3
"""Durable, private feedback requests for CARBON.

The local workspace is intentionally useful without any network service. A
separately configured private Feedback Service may receive a carefully scoped
copy of a request for an invited reviewer; no public CARBON cloud is involved.
"""

from __future__ import annotations

import argparse
import datetime as dt
import hashlib
import json
import os
import tempfile
from pathlib import Path
from typing import Any


SCHEMA = "carbon.feedback-context/v1"
KINDS = {"url", "image", "document", "api", "code", "other"}


def now() -> str:
    return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def read_json(path: Path, fallback: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return fallback


def atomic_json(path: Path, value: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
        json.dump(value, handle, indent=2, ensure_ascii=False)
        handle.write("\n")
        temporary = Path(handle.name)
    os.chmod(temporary, 0o600)
    os.replace(temporary, path)
    os.chmod(path, 0o600)


def context_path(root: Path) -> Path:
    return root / ".carbon" / "feedback" / "requests.json"


def clean(value: str, field: str, limit: int, required: bool = False) -> str:
    result = " ".join(str(value or "").split())
    if required and not result:
        raise ValueError(f"{field} is required")
    if len(result) > limit:
        raise ValueError(f"{field} must be {limit:,} characters or fewer")
    return result


def request_id(seed: str) -> str:
    return "FBK-" + hashlib.sha256(f"{now()}:{seed}".encode("utf-8")).hexdigest()[:10].upper()


def load(root: Path) -> dict[str, Any]:
    value = read_json(context_path(root), {})
    if not isinstance(value, dict) or value.get("schema") != SCHEMA:
        value = {"schema": SCHEMA, "projectRoot": str(root), "createdAt": now(), "requests": []}
    value.setdefault("requests", [])
    return value


def summary(context: dict[str, Any]) -> dict[str, Any]:
    requests = [item for item in context.get("requests", []) if isinstance(item, dict)]
    replies = sum(len(item.get("responses", [])) for item in requests)
    queued = sum(item.get("status") in {"draft", "queued", "sent"} for item in requests)
    return {
        "requests": len(requests),
        "responses": replies,
        "open": queued,
        "delivered": sum(item.get("delivery", {}).get("status") == "sent" for item in requests),
        "localOnly": sum(item.get("delivery", {}).get("mode") == "local" for item in requests),
        "evidenceBoundary": "Feedback is qualitative context. It does not establish that an artifact is correct, secure, or ready to ship.",
    }


def public_request(item: dict[str, Any]) -> dict[str, Any]:
    clone = dict(item)
    # The private-service client key is never stored here. External reviewer
    # links are capabilities and are intentionally returned only after an
    # explicit request was created by the project owner.
    return clone


def create(root: Path, title: str, focus: str, artifact: str, artifact_kind: str, context: str = "", recipient_email: str = "", recipient_name: str = "") -> dict[str, Any]:
    root = root.expanduser().resolve()
    if not root.is_dir():
        raise ValueError("root must be an existing project directory")
    kind = clean(artifact_kind or "other", "artifactKind", 40).lower()
    if kind not in KINDS:
        raise ValueError("artifactKind must be url, image, document, api, code, or other")
    record = {
        "id": request_id(title + focus + artifact),
        "title": clean(title or "CARBON feedback request", "title", 180, True),
        "focus": clean(focus, "focus", 4_000, True),
        "artifact": clean(artifact, "artifact", 4_000),
        "artifactKind": kind,
        "context": clean(context, "context", 12_000),
        "recipient": {"email": clean(recipient_email, "recipientEmail", 320), "name": clean(recipient_name, "recipientName", 180)},
        "status": "draft",
        "createdAt": now(),
        "updatedAt": now(),
        "responses": [],
        "comments": [],
        "delivery": {"mode": "local", "status": "not-requested", "message": "This request remains project-local until an explicitly configured private Feedback Service is used."},
    }
    state = load(root)
    state["requests"].append(record)
    state["requests"] = state["requests"][-100:]
    state["updatedAt"] = now()
    state["summary"] = summary(state)
    atomic_json(context_path(root), state)
    return {"path": str(context_path(root)), "request": record, "summary": state["summary"]}


def status(root: Path, request: str = "") -> dict[str, Any]:
    root = root.expanduser().resolve()
    state = load(root)
    requests = [public_request(row) for row in state.get("requests", []) if isinstance(row, dict)]
    if request:
        matched = next((row for row in requests if row.get("id") == request), None)
        if not matched:
            raise ValueError(f"Unknown feedback request: {request}")
        requests = [matched]
    state["summary"] = summary(state)
    return {"path": str(context_path(root)), "context": state, "requests": requests, "summary": state["summary"]}


def find(state: dict[str, Any], identifier: str) -> dict[str, Any]:
    item = next((row for row in state.get("requests", []) if isinstance(row, dict) and row.get("id") == identifier), None)
    if not item:
        raise ValueError(f"Unknown feedback request: {identifier}")
    return item


def respond(root: Path, request: str, rating: int, response: str, category: str = "feedback", author: str = "local developer") -> dict[str, Any]:
    root = root.expanduser().resolve()
    if rating < 1 or rating > 5:
        raise ValueError("rating must be from 1 to 5")
    state = load(root)
    item = find(state, request)
    record = {
        "id": "FBR-" + hashlib.sha256(f"{now()}:{response}".encode("utf-8")).hexdigest()[:10].upper(),
        "rating": rating,
        "category": clean(category, "category", 80) or "feedback",
        "response": clean(response, "response", 12_000, True),
        "author": clean(author, "author", 180) or "local developer",
        "createdAt": now(),
        "source": "local-workspace",
    }
    item.setdefault("responses", []).append(record)
    item["responses"] = item["responses"][-200:]
    item["status"] = "feedback_ready"
    item["updatedAt"] = now()
    state["updatedAt"] = now()
    state["summary"] = summary(state)
    atomic_json(context_path(root), state)
    return {"path": str(context_path(root)), "request": public_request(item), "response": record, "summary": state["summary"]}


def comment(root: Path, request: str, value: str, author: str = "local developer") -> dict[str, Any]:
    root = root.expanduser().resolve()
    state = load(root)
    item = find(state, request)
    record = {"id": "FBC-" + hashlib.sha256(f"{now()}:{value}".encode("utf-8")).hexdigest()[:10].upper(), "comment": clean(value, "comment", 8_000, True), "author": clean(author, "author", 180) or "local developer", "createdAt": now()}
    item.setdefault("comments", []).append(record)
    item["comments"] = item["comments"][-200:]
    item["updatedAt"] = now()
    state["updatedAt"] = now()
    state["summary"] = summary(state)
    atomic_json(context_path(root), state)
    return {"path": str(context_path(root)), "request": public_request(item), "comment": record, "summary": state["summary"]}


def delivery(root: Path, request: str, status_value: str, message: str, reviewer_url: str = "", service_review_id: str = "") -> dict[str, Any]:
    root = root.expanduser().resolve()
    state = load(root)
    item = find(state, request)
    item["delivery"] = {"mode": "private-service", "status": clean(status_value, "delivery status", 60) or "queued", "message": clean(message, "delivery message", 1_000), "reviewerUrl": clean(reviewer_url, "reviewer URL", 2_048), "serviceReviewId": clean(service_review_id, "service review ID", 180), "updatedAt": now()}
    item["status"] = "sent" if item["delivery"]["status"] == "sent" else "queued"
    item["updatedAt"] = now()
    state["updatedAt"] = now()
    state["summary"] = summary(state)
    atomic_json(context_path(root), state)
    return {"path": str(context_path(root)), "request": public_request(item), "summary": state["summary"]}


def sync(root: Path, request: str, responses_json: str) -> dict[str, Any]:
    """Bring redacted reviewer responses back from the owner-approved private service."""
    root = root.expanduser().resolve()
    try:
        remote = json.loads(responses_json)
    except json.JSONDecodeError as error:
        raise ValueError("responsesJson must contain a JSON response list") from error
    if not isinstance(remote, list):
        raise ValueError("responsesJson must contain a list")
    state = load(root)
    item = find(state, request)
    existing = {str(row.get("externalId")) for row in item.get("responses", []) if isinstance(row, dict) and row.get("externalId")}
    imported: list[dict[str, Any]] = []
    for raw in remote[:200]:
        if not isinstance(raw, dict):
            continue
        external_id = clean(raw.get("id"), "response.id", 180)
        if not external_id or external_id in existing:
            continue
        rating = raw.get("rating")
        if not isinstance(rating, int) or rating < 1 or rating > 5:
            continue
        response = clean(raw.get("response"), "response", 12_000, True)
        record = {"id": "FBR-" + hashlib.sha256(f"{now()}:{external_id}".encode("utf-8")).hexdigest()[:10].upper(), "externalId": external_id, "rating": rating, "category": clean(raw.get("category") or "feedback", "category", 80), "response": response, "author": "private reviewer", "createdAt": clean(raw.get("createdAt") or now(), "createdAt", 80), "source": "private-feedback-service"}
        item.setdefault("responses", []).append(record)
        imported.append(record)
    item["responses"] = item.get("responses", [])[-200:]
    if imported:
        item["status"] = "feedback_ready"
    item["updatedAt"] = now()
    state["updatedAt"] = now()
    state["summary"] = summary(state)
    atomic_json(context_path(root), state)
    return {"path": str(context_path(root)), "request": public_request(item), "importedResponses": imported, "summary": state["summary"]}


def render_report(root: Path) -> dict[str, Any]:
    root = root.expanduser().resolve()
    current = status(root)
    requests = current["requests"]
    request_items = []
    response_items = []
    for item in requests:
        delivery_info = item.get("delivery", {})
        request_items.append({"title": f"{item.get('artifactKind', 'artifact').title()} · {item.get('title')}", "detail": item.get("focus") or "No focus recorded.", "evidence": f"Delivery: {delivery_info.get('status', 'not requested')}. {delivery_info.get('message', '')}", "status": "completed" if item.get("responses") else "planned"})
        for response in item.get("responses", []):
            response_items.append({"title": f"{response.get('category', 'feedback').title()} · {response.get('rating', '?')}/5", "detail": response.get("response", ""), "evidence": f"Recorded by {response.get('author', 'reviewer')} at {response.get('createdAt', '')}.", "status": "completed"})
    payload = {
        "command": "carbon-feedback",
        "title": "Private feedback loop",
        "summary": "CARBON prepared a scoped feedback request and retained responses as qualitative project context.",
        "status": "completed" if response_items else "partial",
        "scope": root.name,
        "metrics": [
            {"label": "Requests", "value": str(len(requests)), "detail": "Scoped feedback invitations retained locally.", "tone": "neutral", "icon": "context"},
            {"label": "Responses", "value": str(len(response_items)), "detail": "Qualitative observations returned to the project.", "tone": "good" if response_items else "neutral", "icon": "observation"},
            {"label": "Private deliveries", "value": str(current["summary"]["delivered"]), "detail": "Only explicitly configured private service deliveries.", "tone": "neutral", "icon": "lock"},
            {"label": "Local only", "value": str(current["summary"]["localOnly"]), "detail": "Requests that never left this project.", "tone": "good", "icon": "evidence"},
        ],
        "sections": [
            {"title": "Feedback requests", "intro": "Each request names the artifact and the decision-relevant focus. CARBON sends no public-cloud request.", "icon": "context", "tone": "neutral", "items": request_items},
            {"title": "Recorded feedback", "intro": "Responses are qualitative evidence and should steer testing, design, and human decisions—not be mistaken for automated verification.", "icon": "observation", "tone": "good" if response_items else "warning", "items": response_items},
            {"title": "Private delivery boundary", "intro": "A remote recipient is invited only through the separately deployed private Feedback Service. The local request file stores no service access key; private links are capability URLs and should be shared only with the intended reviewer.", "icon": "lock", "tone": "warning", "bullets": ["Email uses a rich invitation that opens a protected feedback page; webmail clients do not reliably support safe embedded forms.", "Local developer responses remain in the project’s .carbon state and are available to later CARBON workflows."]},
        ],
        "artifacts": [{"label": "Feedback request state", "path": str(context_path(root)), "detail": "Project-local requests, notes, responses, and non-secret delivery status.", "icon": "artifact"}],
        "nextActions": ["Use the interactive Feedback workspace to record local developer feedback.", "Run /carbon-human or /carbon-confidence after meaningful feedback changes the customer, risk, or release interpretation."],
        "specialistIds": ["jason"],
        "evidenceBoundary": current["summary"]["evidenceBoundary"],
    }
    from carbon_command_report import render
    rendered = render(root, payload)
    state = load(root)
    state["latestReportPath"] = str(rendered.get("reportPath") or "")
    state["latestReportGeneratedAt"] = now()
    state["summary"] = summary(state)
    atomic_json(context_path(root), state)
    return {"path": str(context_path(root)), "context": state, "summary": current["summary"], "report": rendered}


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(description=__doc__)
    sub = root.add_subparsers(dest="action", required=True)
    created = sub.add_parser("create")
    created.add_argument("--root", required=True); created.add_argument("--title", default="CARBON feedback request"); created.add_argument("--focus", required=True); created.add_argument("--artifact", default=""); created.add_argument("--artifact-kind", default="other"); created.add_argument("--context", default=""); created.add_argument("--recipient-email", default=""); created.add_argument("--recipient-name", default="")
    created.set_defaults(func=lambda a: create(Path(a.root), a.title, a.focus, a.artifact, a.artifact_kind, a.context, a.recipient_email, a.recipient_name))
    queried = sub.add_parser("status"); queried.add_argument("--root", required=True); queried.add_argument("--request", default=""); queried.set_defaults(func=lambda a: status(Path(a.root), a.request))
    response = sub.add_parser("respond"); response.add_argument("--root", required=True); response.add_argument("--request", required=True); response.add_argument("--rating", type=int, required=True); response.add_argument("--response", required=True); response.add_argument("--category", default="feedback"); response.add_argument("--author", default="local developer"); response.set_defaults(func=lambda a: respond(Path(a.root), a.request, a.rating, a.response, a.category, a.author))
    noted = sub.add_parser("comment"); noted.add_argument("--root", required=True); noted.add_argument("--request", required=True); noted.add_argument("--comment", required=True); noted.add_argument("--author", default="local developer"); noted.set_defaults(func=lambda a: comment(Path(a.root), a.request, a.comment, a.author))
    delivered = sub.add_parser("delivery"); delivered.add_argument("--root", required=True); delivered.add_argument("--request", required=True); delivered.add_argument("--status", required=True); delivered.add_argument("--message", default=""); delivered.add_argument("--reviewer-url", default=""); delivered.add_argument("--service-review-id", default=""); delivered.set_defaults(func=lambda a: delivery(Path(a.root), a.request, a.status, a.message, a.reviewer_url, a.service_review_id))
    synced = sub.add_parser("sync"); synced.add_argument("--root", required=True); synced.add_argument("--request", required=True); synced.add_argument("--responses-json", required=True); synced.set_defaults(func=lambda a: sync(Path(a.root), a.request, a.responses_json))
    reported = sub.add_parser("report"); reported.add_argument("--root", required=True); reported.set_defaults(func=lambda a: render_report(Path(a.root)))
    return root


def main() -> int:
    try:
        args = parser().parse_args()
        print(json.dumps(args.func(args), ensure_ascii=False, indent=2))
        return 0
    except (OSError, ValueError) as error:
        print(f"carbon_feedback: {error}")
        return 2


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