"""Machine-readable manifest for values the user must supply (Calendly URL, public IDs, etc.).

Cursor is instructed (see ``prompt_wrapper._CURSOR_BEHAVIOR``) to create
``p2hs/required-user-inputs.json`` when the implementation needs configuration
the agent cannot invent. The worker reads it after codegen and stores a copy on
the ``codegen_runs`` row for the dashboard; the user submits values via
``POST .../supply-user-inputs`` or chat continuation, which merge into
``project_settings.settings.builder_user_values`` (DB). Follow-up runs filter manifest
items already present there so a stale or gitignored file does not re-block forever.
"""
from __future__ import annotations

import json
import os
from typing import Any, Optional

from src.core.exceptions import ValidationError

MANIFEST_RELPATH = "p2hs/required-user-inputs.json"


def manifest_path(repo_path: str) -> str:
    return os.path.join(repo_path, *MANIFEST_RELPATH.split("/"))


def load_manifest_from_repo_path(repo_path: str) -> Optional[dict[str, Any]]:
    """Load and validate manifest from a checkout. Returns None if missing or invalid."""
    p = manifest_path(repo_path)
    if not os.path.isfile(p):
        return None
    try:
        with open(p, encoding="utf-8") as f:
            data = json.load(f)
    except (OSError, json.JSONDecodeError, UnicodeDecodeError):
        return None
    if not isinstance(data, dict):
        return None
    ver = data.get("version")
    if ver is not None and ver != 1:
        return None
    items = data.get("items")
    if not isinstance(items, list) or len(items) == 0:
        return None
    filtered: list[dict[str, Any]] = []
    for it in items:
        if not isinstance(it, dict) or not (it.get("id") and str(it["id"]).strip()):
            return None
        if not it.get("label"):
            return None
        # type is optional: "secret" | "public" — defaults to "public" if absent
        if "type" in it and it["type"] not in ("secret", "public"):
            return None
        item_id = str(it["id"]).strip().upper()
        # Platform injects generated-app Turnstile keys. Never block a run on them.
        if item_id.startswith("TURNSTILE"):
            continue
        filtered.append(it)
    if not filtered:
        return None
    data = dict(data)
    data["items"] = filtered
    return data


def validate_submitted_values(manifest: dict[str, Any], values: dict[str, str]) -> None:
    """Raise ValidationError if keys don't match manifest item ids or required ids are missing."""
    items = manifest.get("items") or []
    ids = {str(it["id"]) for it in items if isinstance(it, dict) and it.get("id")}
    for k in values:
        if k not in ids:
            raise ValidationError(f"Unknown field: {k}")
    for iid in ids:
        if iid not in values or not str(values[iid]).strip():
            raise ValidationError(f"Missing or empty value for: {iid}")
