#!/usr/bin/env python3
"""Deterministic control plane for delegated PRD Plugin reporting (REQ-084).

PRD Plugin owns policy, sanitized input bundles, result validation, and fallback
semantics.  It never calls a model.  An external executor (currently
``ai-collab``) reads the bundle, resolves the portable model profile, and returns
a source-backed result that this module validates without writing project truth.
"""

from __future__ import annotations

import argparse
import json
import sys
from collections import Counter
from pathlib import Path
from typing import Any

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

import prd_config


CONTRACT_VERSION = 1
POLICY_PREFIX = "reporting.delegation."
CONFIDENCE_LEVELS = {"low", "medium", "high"}


class PolicyError(ValueError):
    """The canonical config contains an invalid delegation policy."""


class ResultError(ValueError):
    """A delegated model result does not satisfy the reporting contract."""


def _read_json(path: Path, default: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8-sig"))
    except FileNotFoundError:
        return default
    except (OSError, json.JSONDecodeError) as exc:
        raise PolicyError(f"cannot read valid JSON from {path}: {exc}") from exc


def _delegation_specs() -> list[dict[str, Any]]:
    return [spec for spec in prd_config.TOGGLES if spec["key"].startswith(POLICY_PREFIX)]


def effective_policy(repo_root: str | Path = ".") -> dict[str, Any]:
    """Return the validated effective policy from defaults + direct JSON edits."""
    root = Path(repo_root)
    config = _read_json(root / ".prd_plugin" / "config.json", {})
    if not isinstance(config, dict):
        raise PolicyError(".prd_plugin/config.json must contain a JSON object")
    raw = config.get("reporting", {})
    raw = raw.get("delegation", {}) if isinstance(raw, dict) else None
    if raw is None or not isinstance(raw, dict):
        raise PolicyError("reporting.delegation must be a JSON object")

    known = {spec["key"].removeprefix(POLICY_PREFIX) for spec in _delegation_specs()}
    unknown = sorted(set(raw) - known)
    if unknown:
        raise PolicyError(f"unknown reporting.delegation field(s): {unknown}")

    policy: dict[str, Any] = {}
    for spec in _delegation_specs():
        name = spec["key"].removeprefix(POLICY_PREFIX)
        value = raw[name] if name in raw else spec["default"]
        try:
            policy[name] = prd_config._coerce(spec, value)
        except ValueError as exc:
            raise PolicyError(str(exc)) from exc
    if policy["contract_version"] != CONTRACT_VERSION:
        raise PolicyError(
            f"unsupported reporting contract_version {policy['contract_version']}; "
            f"supported: {CONTRACT_VERSION}")
    return policy


def delegation_decision(
    repo_root: str | Path,
    task: str,
    executor_available: bool,
) -> dict[str, Any]:
    """Resolve delegate vs configured fallback for one known reporting task."""
    if task not in prd_config.REPORTING_TASKS:
        raise PolicyError(f"unknown reporting task {task!r}")
    policy = effective_policy(repo_root)
    if policy["enabled"] and task in policy["tasks"] and executor_available:
        return {"action": "delegate", "reason": "enabled_allowed_available", "policy": policy}
    if not policy["enabled"]:
        reason = "disabled"
    elif task not in policy["tasks"]:
        reason = "task_not_allowed"
    else:
        reason = "executor_unavailable"
    return {"action": policy["fallback"], "reason": reason, "policy": policy}


def _records(root: Path, filename: str, keys: tuple[str, ...]) -> list[dict[str, Any]]:
    data = _read_json(root / ".prd_plugin" / "state" / filename, {})
    if not isinstance(data, dict):
        return []
    for key in keys:
        if isinstance(data.get(key), list):
            return [item for item in data[key] if isinstance(item, dict)]
    return []


def _compact(record: dict[str, Any], fields: tuple[str, ...]) -> dict[str, Any]:
    return {field: record.get(field) for field in fields if record.get(field) is not None}


def build_bundle(repo_root: str | Path = ".", task: str = "report_summary") -> dict[str, Any]:
    """Build a deterministic, sanitized reporting input bundle for AI-Collab."""
    root = Path(repo_root).resolve()
    policy = effective_policy(root)
    if task not in prd_config.REPORTING_TASKS:
        raise PolicyError(f"unknown reporting task {task!r}")
    if task not in policy["tasks"]:
        raise PolicyError(f"reporting task {task!r} is not allowed by reporting.delegation.tasks")

    requests = _records(root, "requests.json", ("requests", "records"))
    tracking = _records(root, "tracking.json", ("records", "tracking"))
    health = _records(root, "health.json", ("findings", "records"))
    evidence = _records(root, "evidence.json", ("records", "evidence"))
    decisions = _records(root, "decisions.json", ("decisions", "records"))

    active_statuses = {"open", "active", "in_progress", "proposed", "in_review", "needs_info", "accepted"}
    active_requests = [r for r in requests if str(r.get("status", "")).lower() in active_statuses]
    active_tracking = [r for r in tracking if str(r.get("status", "")).lower() in active_statuses]
    open_health = [r for r in health if str(r.get("status", "open")).lower() in {"open", "active"}]

    request_rows = [_compact(r, ("id", "status", "severity", "summary")) for r in active_requests[:50]]
    tracking_rows = [_compact(r, ("id", "type", "status", "summary")) for r in active_tracking[:50]]
    health_rows = [_compact(r, ("id", "status", "severity", "summary", "recommended_action")) for r in open_health[:50]]
    ids = [row.get("id") for row in request_rows + tracking_rows + health_rows if row.get("id")]
    source_refs = sorted(set(ids + [
        ".prd_plugin/config.json",
        ".prd_plugin/state/requests.json",
        ".prd_plugin/state/tracking.json",
        ".prd_plugin/state/health.json",
        ".prd_plugin/state/evidence.json",
        ".prd_plugin/state/decisions.json",
    ]))

    findings = [
        {"id": row.get("id"), "severity": row.get("severity", "medium"),
         "summary": row.get("summary", ""), "source_refs": [row.get("id")]}
        for row in health_rows if row.get("id")
    ]
    return {
        "contract_version": CONTRACT_VERSION,
        "task": task,
        "repo": root.name,
        "policy": policy,
        "facts": {
            "requests_by_status": dict(sorted(Counter(str(r.get("status", "unknown")) for r in requests).items())),
            "active_requests": request_rows,
            "active_tracking": tracking_rows,
            "open_health": health_rows,
            "evidence_count": len(evidence),
            "decision_count": len(decisions),
        },
        "findings": findings,
        "source_refs": source_refs,
        "constraints": {
            "max_input_tokens": policy["max_input_tokens"],
            "max_output_tokens": policy["max_output_tokens"],
            "timeout_seconds": policy["timeout_seconds"],
            "require_source_refs": policy["require_source_refs"],
            "no_project_state_writes": True,
            "no_secrets_or_local_session_content": True,
        },
    }


def _validate_sourced_rows(
    name: str,
    rows: Any,
    allowed_sources: set[str],
    require_sources: bool,
) -> list[dict[str, Any]]:
    if not isinstance(rows, list):
        raise ResultError(f"{name} must be an array")
    normalized = []
    for index, row in enumerate(rows):
        if not isinstance(row, dict) or not isinstance(row.get("text"), str) or not row["text"].strip():
            raise ResultError(f"{name}[{index}] must contain non-empty text")
        refs = row.get("source_refs", [])
        if not isinstance(refs, list) or any(not isinstance(ref, str) for ref in refs):
            raise ResultError(f"{name}[{index}].source_refs must be an array of strings")
        if require_sources and not refs:
            raise ResultError(f"{name}[{index}] requires source_refs")
        unknown = sorted(set(refs) - allowed_sources)
        if unknown:
            raise ResultError(f"{name}[{index}] references unknown sources: {unknown}")
        normalized.append({"text": row["text"].strip(), "source_refs": refs})
    return normalized


def validate_result(bundle: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
    """Validate and normalize an executor result; never writes repository state."""
    if not isinstance(bundle, dict) or not isinstance(result, dict):
        raise ResultError("bundle and result must be JSON objects")
    for field in ("contract_version", "task"):
        if result.get(field) != bundle.get(field):
            raise ResultError(f"result {field} does not match bundle")
    policy = bundle.get("policy", {})
    if result.get("executor") != policy.get("executor"):
        raise ResultError("result executor does not match policy")
    if result.get("profile") != policy.get("profile"):
        raise ResultError("result profile does not match policy")
    summary = result.get("summary")
    if not isinstance(summary, str) or not summary.strip():
        raise ResultError("result summary must be a non-empty string")
    confidence = str(result.get("confidence", "")).lower()
    if confidence not in CONFIDENCE_LEVELS:
        raise ResultError(f"result confidence must be one of {sorted(CONFIDENCE_LEVELS)}")
    allowed_sources = {ref for ref in bundle.get("source_refs", []) if isinstance(ref, str)}
    require_sources = bool(policy.get("require_source_refs", True))
    return {
        "contract_version": bundle["contract_version"],
        "task": bundle["task"],
        "executor": policy["executor"],
        "profile": policy["profile"],
        "summary": summary.strip(),
        "notable_items": _validate_sourced_rows(
            "notable_items", result.get("notable_items", []), allowed_sources, require_sources),
        "recommended_actions": _validate_sourced_rows(
            "recommended_actions", result.get("recommended_actions", []), allowed_sources, require_sources),
        "confidence": confidence,
    }


def _emit(data: dict[str, Any], fmt: str) -> None:
    if fmt == "json":
        print(json.dumps(data, indent=2, ensure_ascii=False))
    else:
        print(data.get("summary") or json.dumps(data, indent=2, ensure_ascii=False))


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--repo-root", default=".")
    sub = parser.add_subparsers(dest="action", required=True)
    for name in ("policy", "bundle", "decision", "validate-result"):
        command = sub.add_parser(name)
        command.add_argument("--format", choices=("json", "markdown"), default="json")
        if name in ("bundle", "decision"):
            command.add_argument("--task", default="report_summary", choices=prd_config.REPORTING_TASKS)
        if name == "decision":
            command.add_argument("--executor-available", action="store_true")
        if name == "validate-result":
            command.add_argument("--bundle", required=True)
            command.add_argument("--result", required=True)
    args = parser.parse_args(argv)
    try:
        if args.action == "policy":
            data = effective_policy(args.repo_root)
        elif args.action == "bundle":
            data = build_bundle(args.repo_root, args.task)
        elif args.action == "decision":
            data = delegation_decision(args.repo_root, args.task, args.executor_available)
        else:
            data = validate_result(
                _read_json(Path(args.bundle), None),
                _read_json(Path(args.result), None),
            )
        _emit(data, args.format)
        return 0
    except (PolicyError, ResultError) as exc:
        print(f"[PRD Plugin] reporting error: {exc}", file=sys.stderr)
        return 2


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