"""Loader for p2hs/feature-packs.json — the codegen agent's declarative pack-install file.

The agent writes this file when it wants to install a whole-workflow feature pack
(contact-capture, appointment-booking, payment-checkout, ...) for the project. The worker
reads it after the AI run and installs each entry via PackInstallService. Processing is
non-blocking: failures are logged but do not fail the run — mirrors functions_manifest.py.

File shape (version 1):
{
  "version": 1,
  "install": [
    { "pack_slug": "contact-capture", "config": { "notify_email": true } }
  ],
  "compose": [
    {
      "name": "quote_request",
      "actors": ["anonymous", "admin"],
      "requirements": [
        { "action": "create", "resource": "quote_request", "actor": "anonymous" },
        { "action": "read", "resource": "quote_request", "actor": "admin" }
      ],
      "fields": [
        { "key": "name", "label": "Name", "type": "text", "required": true }
      ]
    }
  ]
}

`compose` entries are for a requirement no existing catalog pack covers -- the agent
declares WHAT is needed (actors/requirements/fields, see requirements_spec.py), never
HOW it's enforced. An invalid `compose` entry is dropped (logged, not raised) same as an
invalid `install` entry -- one bad declaration must never break the rest of the file.
"""
from __future__ import annotations

import json
import os
import re
from typing import Any, Optional

from pydantic import ValidationError

MANIFEST_RELPATH = "p2hs/feature-packs.json"
_SLUG_RE = re.compile(r"^[a-z][a-z0-9-]*$")


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


def _load_data(repo_path: str) -> Optional[dict]:
    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
    return data


def load_manifest_from_repo_path(repo_path: str) -> Optional[list[dict[str, Any]]]:
    """Load and validate p2hs/feature-packs.json. Returns the install list or None if absent/invalid."""
    data = _load_data(repo_path)
    if data is None:
        return None
    install = data.get("install")
    if not isinstance(install, list) or not install:
        return None

    valid: list[dict[str, Any]] = []
    for entry in install:
        if not isinstance(entry, dict):
            continue
        pack_slug = entry.get("pack_slug", "")
        if not pack_slug or not _SLUG_RE.match(str(pack_slug)):
            continue
        config = entry.get("config")
        valid.append({
            "pack_slug": str(pack_slug),
            "config": config if isinstance(config, dict) else {},
        })
    return valid if valid else None


def load_compositions_from_repo_path(repo_path: str) -> Optional[list]:
    """Load the raw `compose` entries, unvalidated. Returns None if the array is absent.

    The file is written by the coding agent, so an entry is intent, not a contract. It is
    deliberately NOT validated here: an entry that failed validation used to be dropped
    silently, so a required capability could vanish without any record. Validation, safe
    repair and reporting happen in one place, _process_pack_compositions (via
    manifest_repair.repair_composition), for the local and the GitHub Actions path alike.
    Every list item is returned, including non-objects, so each is reported, not skipped."""
    data = _load_data(repo_path)
    if data is None:
        return None
    compose = data.get("compose")
    if not isinstance(compose, list) or not compose:
        return None
    return list(compose)
