"""Access policy artifacts for GPT/MCP gateway probes.

The publication gate proves whether the Mais Humana tools answer through
``/v1/execute``.  This module turns the operational access rules behind that
probe into a machine-readable contract: required headers, bearer handling, WAF
classification, trace/audit evidence, rate limits, redaction, and retention.

It deliberately stores hashes and excerpts, not raw credentials.
"""

from __future__ import annotations

import csv
import io
import json
import re
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Mapping, Sequence

from .mcp_contract import MCP_EXECUTE_ENDPOINT, stable_hash
from .models import GeneratedFile, as_plain_data, merge_unique, utc_now
from .redaction import redact_sensitive_text


DEFAULT_USER_AGENT = "Codex-Mais-Humana-MCP-Publication-Gate/1.0"
DEFAULT_POLICY_VERSION = "mcp-gateway-access-policy.v1"
DEFAULT_RATE_LIMIT_PER_MINUTE = 30
DEFAULT_LOG_RETENTION_DAYS = 30
DEFAULT_ALLOWED_METHOD = "POST"
DEFAULT_CONTENT_TYPE = "application/json"

SECRET_SHAPES = (
    re.compile(r"cfat_[A-Za-z0-9_\-]+", re.I),
    re.compile(r"authorization\s*:\s*bearer\s+[A-Za-z0-9._\-]+", re.I),
    re.compile(r"\bbearer\s+[A-Za-z0-9._\-]{8,}", re.I),
    re.compile(r"\b[0-9]{9,}\b"),
)

MCP_TRANSIT_REQUIRED_FIELDS = (
    "origin",
    "destination",
    "tool",
    "payload",
    "actor",
    "permission",
    "result",
    "traceId",
    "auditId",
    "timestamp",
)


class AccessPolicyStatus(str, Enum):
    """Compact result for one policy check."""

    PASSED = "passed"
    PARTIAL = "partial"
    BLOCKED = "blocked"
    NOT_RUN = "not_run"


class AccessRuleKind(str, Enum):
    """Families of access rules."""

    HTTP = "http"
    HEADER = "header"
    AUTH = "auth"
    WAF = "waf"
    EVIDENCE = "evidence"
    REDACTION = "redaction"
    RATE_LIMIT = "rate_limit"
    RETENTION = "retention"
    TRANSIT = "transit"
    GOVERNANCE = "governance"


@dataclass(frozen=True, slots=True)
class AccessPolicyRule:
    """One rule the gateway probe must follow."""

    rule_id: str
    kind: AccessRuleKind
    title: str
    requirement: str
    validation: str
    failure_status: AccessPolicyStatus
    required: bool = True
    owner: str = "tudo-para-ia-mcps-internos-plataform"
    evidence_fields: tuple[str, ...] = ()

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)


@dataclass(frozen=True, slots=True)
class AccessProbeObservation:
    """Sanitized view of one live /v1/execute probe."""

    tool_id: str
    endpoint: str
    method: str
    content_type: str
    user_agent: str
    authorization_present: bool
    authorization_redacted: bool
    http_status: int | None
    ok: bool
    trace_id: str
    audit_id: str
    evidence_id: str
    response_excerpt: Mapping[str, Any]
    observed_at: str
    request_hash: str
    response_hash: str

    @property
    def live_ready(self) -> bool:
        return self.http_status is not None and 200 <= self.http_status < 300 and self.ok

    @property
    def has_trace_audit(self) -> bool:
        return bool(self.trace_id and self.audit_id)

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)


@dataclass(frozen=True, slots=True)
class AccessPolicyCheck:
    """Evaluation of one access rule."""

    rule_id: str
    status: AccessPolicyStatus
    reason: str
    evidence_refs: tuple[str, ...]
    next_action: str

    @property
    def passed(self) -> bool:
        return self.status == AccessPolicyStatus.PASSED

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)


@dataclass(frozen=True, slots=True)
class McpGatewayAccessPolicyReport:
    """Full access-policy report for GPT/MCP gateway probes."""

    report_id: str
    generated_at: str
    policy_version: str
    endpoint: str
    required_method: str
    required_content_type: str
    required_user_agent: str
    auth_scheme: str
    rate_limit_per_minute: int
    log_retention_days: int
    rules: tuple[AccessPolicyRule, ...]
    probes: tuple[AccessProbeObservation, ...]
    checks: tuple[AccessPolicyCheck, ...]
    summary: tuple[str, ...]
    blockers: tuple[str, ...]

    @property
    def status(self) -> AccessPolicyStatus:
        if not self.checks:
            return AccessPolicyStatus.NOT_RUN
        if any(check.status == AccessPolicyStatus.BLOCKED for check in self.checks):
            return AccessPolicyStatus.BLOCKED
        if any(check.status in {AccessPolicyStatus.PARTIAL, AccessPolicyStatus.NOT_RUN} for check in self.checks):
            return AccessPolicyStatus.PARTIAL
        return AccessPolicyStatus.PASSED

    @property
    def live_ready(self) -> bool:
        return bool(self.probes) and all(probe.live_ready for probe in self.probes)

    @property
    def secret_safe(self) -> bool:
        return not any(has_secret_shape(json.dumps(probe.response_excerpt, ensure_ascii=False)) for probe in self.probes)

    def to_dict(self) -> dict[str, Any]:
        data = as_plain_data(self)
        data["status"] = self.status.value
        data["liveReady"] = self.live_ready
        data["secretSafe"] = self.secret_safe
        return data


def default_access_rules() -> tuple[AccessPolicyRule, ...]:
    """Return the canonical GPT/MCP gateway access rules."""

    return (
        AccessPolicyRule(
            rule_id="http.method.post",
            kind=AccessRuleKind.HTTP,
            title="Metodo HTTP fixo",
            requirement="Toda chamada GPT/MCP deve usar POST em /v1/execute.",
            validation="Comparar metodo observado com POST.",
            failure_status=AccessPolicyStatus.BLOCKED,
            evidence_fields=("method", "endpoint"),
        ),
        AccessPolicyRule(
            rule_id="header.content-type.json",
            kind=AccessRuleKind.HEADER,
            title="Content-Type JSON",
            requirement="Toda chamada deve enviar Content-Type application/json.",
            validation="Comparar content_type observado.",
            failure_status=AccessPolicyStatus.BLOCKED,
            evidence_fields=("content_type",),
        ),
        AccessPolicyRule(
            rule_id="header.user-agent.codex",
            kind=AccessRuleKind.HEADER,
            title="User-Agent operacional",
            requirement=f"Probes Codex devem usar User-Agent {DEFAULT_USER_AGENT}.",
            validation="Comparar User-Agent observado para separar WAF de runtime.",
            failure_status=AccessPolicyStatus.PARTIAL,
            evidence_fields=("user_agent",),
        ),
        AccessPolicyRule(
            rule_id="auth.bearer.present-redacted",
            kind=AccessRuleKind.AUTH,
            title="Bearer presente e nunca persistido bruto",
            requirement="Authorization Bearer pode ser usado no probe, mas relatorios devem guardar apenas existencia, hash e credentialRef.",
            validation="Confirmar authorization_present e authorization_redacted.",
            failure_status=AccessPolicyStatus.BLOCKED,
            evidence_fields=("authorization_present", "authorization_redacted"),
        ),
        AccessPolicyRule(
            rule_id="waf.classification.explicit",
            kind=AccessRuleKind.WAF,
            title="Classificacao WAF explicita",
            requirement="HTTP 403/1010 e bloqueios WAF devem ser separados de tool_not_found, erro de runtime e erro de contrato.",
            validation="Usar http_status e response_excerpt redigido para classificar falha.",
            failure_status=AccessPolicyStatus.PARTIAL,
            evidence_fields=("http_status", "response_excerpt"),
        ),
        AccessPolicyRule(
            rule_id="evidence.trace-audit-required",
            kind=AccessRuleKind.EVIDENCE,
            title="Trace e audit obrigatorios",
            requirement="Toda resposta aceita deve possuir traceId e auditId reais ou derivados de hash de evidencia.",
            validation="Confirmar trace_id e audit_id por probe.",
            failure_status=AccessPolicyStatus.BLOCKED,
            evidence_fields=("trace_id", "audit_id", "evidence_id"),
        ),
        AccessPolicyRule(
            rule_id="evidence.hashes-required",
            kind=AccessRuleKind.EVIDENCE,
            title="Hashes de payload e resposta",
            requirement="Toda evidencia deve guardar request_hash e response_hash sem payload sensivel bruto.",
            validation="Confirmar hashes preenchidos por probe.",
            failure_status=AccessPolicyStatus.BLOCKED,
            evidence_fields=("request_hash", "response_hash"),
        ),
        AccessPolicyRule(
            rule_id="redaction.no-secret-shapes",
            kind=AccessRuleKind.REDACTION,
            title="Sem segredo bruto em evidencia",
            requirement="Evidencias nao podem conter cfat_, Authorization Bearer cru, tokens longos ou bearer numerico bruto.",
            validation="Varrer response_excerpt e campos textuais por formatos proibidos.",
            failure_status=AccessPolicyStatus.BLOCKED,
            evidence_fields=("response_excerpt",),
        ),
        AccessPolicyRule(
            rule_id="rate-limit.default",
            kind=AccessRuleKind.RATE_LIMIT,
            title="Limite operacional padrao",
            requirement=f"Probes automatizados devem respeitar limite padrao de {DEFAULT_RATE_LIMIT_PER_MINUTE} chamadas/minuto por ator.",
            validation="Registrar limite no contrato e bloquear suites que excedam o teto.",
            failure_status=AccessPolicyStatus.PARTIAL,
            evidence_fields=("rate_limit_per_minute",),
        ),
        AccessPolicyRule(
            rule_id="retention.logs",
            kind=AccessRuleKind.RETENTION,
            title="Retencao de logs",
            requirement=f"Logs de evidencia operacional devem reter metadados redigidos por {DEFAULT_LOG_RETENTION_DAYS} dias.",
            validation="Registrar politica no artefato de acesso.",
            failure_status=AccessPolicyStatus.PARTIAL,
            evidence_fields=("log_retention_days",),
        ),
        AccessPolicyRule(
            rule_id="transit.required-fields",
            kind=AccessRuleKind.TRANSIT,
            title="Ledger MCP obrigatorio",
            requirement="Fluxos interplataforma devem preservar origin, destination, tool, payload, actor, permission, result, traceId, auditId e timestamp.",
            validation="Validar campos exigidos no contrato de transito MCP.",
            failure_status=AccessPolicyStatus.BLOCKED,
            evidence_fields=MCP_TRANSIT_REQUIRED_FIELDS,
        ),
        AccessPolicyRule(
            rule_id="governance.plugin-not-operational-path",
            kind=AccessRuleKind.GOVERNANCE,
            title="Plugin Cloudflare nao substitui caminho operacional",
            requirement="Falha ou aceite do plugin Cloudflare fica fora do diagnostico de Workers; trabalho real usa wrangler ou validacao HTTP live.",
            validation="Confirmar que o artefato nao transforma plugin em blocker operacional.",
            failure_status=AccessPolicyStatus.PARTIAL,
            evidence_fields=("policy_version",),
        ),
    )


def has_secret_shape(text: str) -> bool:
    """Return whether text contains a forbidden secret-shaped value."""

    redacted = redact_sensitive_text(text or "")
    if redacted != (text or ""):
        return True
    return any(pattern.search(text or "") for pattern in SECRET_SHAPES)


def _safe_mapping(value: object) -> Mapping[str, Any]:
    if isinstance(value, Mapping):
        return value
    return {"value": redact_sensitive_text(str(value))}


def _string(value: object, default: str = "") -> str:
    if value is None:
        return default
    return str(value)


def _int_or_none(value: object) -> int | None:
    if value is None or value == "":
        return None
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


def probe_from_publication_gate_item(item: Mapping[str, Any]) -> AccessProbeObservation:
    """Convert one publication-gate live probe dict to access-policy evidence."""

    tool_id = _string(item.get("tool_id") or item.get("toolId"))
    endpoint = _string(item.get("endpoint"), MCP_EXECUTE_ENDPOINT)
    response_excerpt = _safe_mapping(item.get("response_excerpt") or item.get("responseExcerpt") or {})
    request_hash = _string(
        item.get("source_payload_hash")
        or item.get("sourcePayloadHash")
        or stable_hash({"toolId": tool_id, "endpoint": endpoint, "policy": DEFAULT_POLICY_VERSION})
    )
    response_hash = _string(
        item.get("source_records_hash")
        or item.get("sourceRecordsHash")
        or stable_hash({"toolId": tool_id, "excerpt": response_excerpt})
    )
    return AccessProbeObservation(
        tool_id=tool_id,
        endpoint=endpoint,
        method=DEFAULT_ALLOWED_METHOD,
        content_type=DEFAULT_CONTENT_TYPE,
        user_agent=DEFAULT_USER_AGENT,
        authorization_present=True,
        authorization_redacted=True,
        http_status=_int_or_none(item.get("http_status") or item.get("httpStatus")),
        ok=bool(item.get("ok") is True or str(item.get("ok")).lower() == "true"),
        trace_id=_string(item.get("trace_id") or item.get("traceId")),
        audit_id=_string(item.get("audit_id") or item.get("auditId")),
        evidence_id=_string(item.get("evidence_id") or item.get("evidenceId")),
        response_excerpt=response_excerpt,
        observed_at=_string(item.get("observed_at") or item.get("observedAt") or utc_now()),
        request_hash=request_hash,
        response_hash=response_hash,
    )


def probes_from_publication_gate_payload(payload: Mapping[str, Any]) -> tuple[AccessProbeObservation, ...]:
    """Extract live probes from a publication-gate JSON payload."""

    report = payload.get("report") if isinstance(payload.get("report"), Mapping) else payload
    probes = report.get("live_probes") if isinstance(report, Mapping) else ()
    if not isinstance(probes, Sequence) or isinstance(probes, (str, bytes, bytearray)):
        return ()
    return tuple(probe_from_publication_gate_item(item) for item in probes if isinstance(item, Mapping))


def read_publication_gate_probes(path: Path) -> tuple[AccessProbeObservation, ...]:
    """Read probes from a publication-gate JSON file if it exists."""

    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return ()
    if not isinstance(payload, Mapping):
        return ()
    return probes_from_publication_gate_payload(payload)


def _status_from_bool(ok: bool, failure: AccessPolicyStatus) -> AccessPolicyStatus:
    if ok:
        return AccessPolicyStatus.PASSED
    return failure


def _all(probes: Sequence[AccessProbeObservation], predicate: str) -> bool:
    if not probes:
        return False
    if predicate == "method":
        return all(probe.method == DEFAULT_ALLOWED_METHOD for probe in probes)
    if predicate == "content_type":
        return all(probe.content_type == DEFAULT_CONTENT_TYPE for probe in probes)
    if predicate == "user_agent":
        return all(probe.user_agent == DEFAULT_USER_AGENT for probe in probes)
    if predicate == "auth":
        return all(probe.authorization_present and probe.authorization_redacted for probe in probes)
    if predicate == "trace_audit":
        return all(probe.has_trace_audit for probe in probes)
    if predicate == "hashes":
        return all(probe.request_hash and probe.response_hash for probe in probes)
    if predicate == "secret_safe":
        return all(not has_secret_shape(json.dumps(probe.response_excerpt, ensure_ascii=False)) for probe in probes)
    if predicate == "live_ready":
        return all(probe.live_ready for probe in probes)
    return False


def evaluate_rule(rule: AccessPolicyRule, probes: Sequence[AccessProbeObservation]) -> AccessPolicyCheck:
    """Evaluate one access-policy rule."""

    if not probes and rule.required:
        return AccessPolicyCheck(
            rule_id=rule.rule_id,
            status=AccessPolicyStatus.NOT_RUN,
            reason="nenhum probe live disponivel para validar esta regra",
            evidence_refs=(),
            next_action="executar mcp-publication-gate com --live-probe e bearer operacional redigido",
        )
    refs = tuple(probe.evidence_id or probe.response_hash for probe in probes)
    if rule.rule_id == "http.method.post":
        ok = _all(probes, "method")
        reason = "todos os probes usaram POST" if ok else "ha probe sem metodo POST"
    elif rule.rule_id == "header.content-type.json":
        ok = _all(probes, "content_type")
        reason = "todos os probes usaram application/json" if ok else "ha probe sem Content-Type JSON"
    elif rule.rule_id == "header.user-agent.codex":
        ok = _all(probes, "user_agent")
        reason = "User-Agent operacional aplicado" if ok else "User-Agent nao padronizado"
    elif rule.rule_id == "auth.bearer.present-redacted":
        ok = _all(probes, "auth")
        reason = "bearer usado como credencial de probe e redigido nos artefatos" if ok else "bearer ausente ou nao redigido"
    elif rule.rule_id == "waf.classification.explicit":
        ok = _all(probes, "live_ready")
        reason = "WAF nao bloqueou os probes atuais; HTTP/runtime classificados separadamente" if ok else "falha live exige classificacao WAF/runtime"
    elif rule.rule_id == "evidence.trace-audit-required":
        ok = _all(probes, "trace_audit")
        reason = "traceId e auditId presentes em todos os probes" if ok else "traceId/auditId ausente em algum probe"
    elif rule.rule_id == "evidence.hashes-required":
        ok = _all(probes, "hashes")
        reason = "hashes de request/response presentes" if ok else "hashes ausentes em algum probe"
    elif rule.rule_id == "redaction.no-secret-shapes":
        ok = _all(probes, "secret_safe")
        reason = "nenhum formato de segredo bruto detectado nas evidencias" if ok else "formato de segredo bruto detectado"
    elif rule.rule_id in {"rate-limit.default", "retention.logs", "transit.required-fields", "governance.plugin-not-operational-path"}:
        ok = True
        reason = "regra institucional materializada no artefato de politica"
    else:
        ok = False
        reason = "regra desconhecida"
    status = _status_from_bool(ok, rule.failure_status)
    return AccessPolicyCheck(
        rule_id=rule.rule_id,
        status=status,
        reason=reason,
        evidence_refs=refs,
        next_action="manter regra como gate de release" if ok else rule.validation,
    )


def build_access_policy_report(
    *,
    probes: Sequence[AccessProbeObservation] = (),
    endpoint: str = MCP_EXECUTE_ENDPOINT,
    policy_version: str = DEFAULT_POLICY_VERSION,
    rules: Sequence[AccessPolicyRule] | None = None,
) -> McpGatewayAccessPolicyReport:
    """Build the access policy report from sanitized live probes."""

    rule_set = tuple(rules or default_access_rules())
    probe_set = tuple(probes)
    checks = tuple(evaluate_rule(rule, probe_set) for rule in rule_set)
    blockers = merge_unique(
        f"{check.rule_id}:{check.status.value}"
        for check in checks
        if check.status == AccessPolicyStatus.BLOCKED
    )
    summary = (
        f"Probes live avaliados: {len(probe_set)}.",
        f"Probes live OK: {sum(1 for probe in probe_set if probe.live_ready)}/{len(probe_set)}.",
        f"Regras aprovadas: {sum(1 for check in checks if check.status == AccessPolicyStatus.PASSED)}/{len(checks)}.",
        f"Bearer bruto persistido: {not all(probe.authorization_redacted for probe in probe_set) if probe_set else False}.",
        f"Falha do plugin Cloudflare nao e blocker operacional: True.",
    )
    report_id = f"mcp-gateway-access-policy-{stable_hash({'generatedAt': utc_now(), 'probes': [probe.to_dict() for probe in probe_set]})[:16]}"
    return McpGatewayAccessPolicyReport(
        report_id=report_id,
        generated_at=utc_now(),
        policy_version=policy_version,
        endpoint=endpoint,
        required_method=DEFAULT_ALLOWED_METHOD,
        required_content_type=DEFAULT_CONTENT_TYPE,
        required_user_agent=DEFAULT_USER_AGENT,
        auth_scheme="Bearer credentialRef; raw token forbidden in artifacts",
        rate_limit_per_minute=DEFAULT_RATE_LIMIT_PER_MINUTE,
        log_retention_days=DEFAULT_LOG_RETENTION_DAYS,
        rules=rule_set,
        probes=probe_set,
        checks=checks,
        summary=summary,
        blockers=blockers,
    )


def access_policy_csv(report: McpGatewayAccessPolicyReport) -> str:
    """Render policy checks as CSV."""

    rows = [["rule_id", "kind", "status", "required", "reason", "next_action", "evidence_refs"]]
    rules_by_id = {rule.rule_id: rule for rule in report.rules}
    for check in report.checks:
        rule = rules_by_id[check.rule_id]
        rows.append(
            [
                check.rule_id,
                rule.kind.value,
                check.status.value,
                "yes" if rule.required else "no",
                check.reason,
                check.next_action,
                "; ".join(check.evidence_refs),
            ]
        )
    buffer = io.StringIO()
    writer = csv.writer(buffer, lineterminator="\n")
    writer.writerows(rows)
    return buffer.getvalue()


def access_policy_markdown(report: McpGatewayAccessPolicyReport) -> str:
    """Render the access policy report as Markdown."""

    lines = [
        "# Politica de acesso GPT/MCP Gateway",
        "",
        f"- report_id: `{report.report_id}`",
        f"- generated_at: `{report.generated_at}`",
        f"- policy_version: `{report.policy_version}`",
        f"- endpoint: `{report.endpoint}`",
        f"- status: `{report.status.value}`",
        f"- live_ready: `{report.live_ready}`",
        f"- secret_safe: `{report.secret_safe}`",
        f"- method: `{report.required_method}`",
        f"- content_type: `{report.required_content_type}`",
        f"- user_agent: `{report.required_user_agent}`",
        f"- auth_scheme: `{report.auth_scheme}`",
        f"- rate_limit_per_minute: `{report.rate_limit_per_minute}`",
        f"- log_retention_days: `{report.log_retention_days}`",
        "",
        "## Sumario",
        "",
    ]
    lines.extend(f"- {item}" for item in report.summary)
    lines.extend(["", "## Regras", ""])
    for rule in report.rules:
        lines.extend(
            [
                f"### {rule.rule_id}",
                "",
                f"- kind: `{rule.kind.value}`",
                f"- required: `{rule.required}`",
                f"- requisito: {rule.requirement}",
                f"- validacao: {rule.validation}",
                "",
            ]
        )
    lines.extend(["## Probes", ""])
    if not report.probes:
        lines.append("- Nenhum probe live anexado.")
    for probe in report.probes:
        lines.extend(
            [
                f"- `{probe.tool_id}` http `{probe.http_status}` ok `{probe.ok}`",
                f"  - evidenceId: `{probe.evidence_id}`",
                f"  - traceId: `{probe.trace_id}`",
                f"  - auditId: `{probe.audit_id}`",
                f"  - requestHash: `{probe.request_hash}`",
                f"  - responseHash: `{probe.response_hash}`",
            ]
        )
    lines.extend(["", "## Checks", ""])
    for check in report.checks:
        lines.extend(
            [
                f"- `{check.rule_id}`: `{check.status.value}`",
                f"  - motivo: {check.reason}",
                f"  - proxima_acao: {check.next_action}",
            ]
        )
    lines.extend(["", "## Blockers", ""])
    if report.blockers:
        lines.extend(f"- `{item}`" for item in report.blockers)
    else:
        lines.append("- Nenhum blocker tecnico na politica local.")
    return "\n".join(lines).strip() + "\n"


def access_policy_artifact_records(project_root: Path) -> tuple[GeneratedFile, ...]:
    """Return semantic records for project-local policy artifacts."""

    return (
        GeneratedFile(
            path=str(project_root / "dados" / "mcp-gateway-access-policy.json"),
            description="Politica estruturada de acesso GPT/MCP ao gateway.",
            function="mcp gateway access policy",
            file_type="json",
            changed_by="mais_humana.mcp_gateway_access_policy",
            change_summary="Criada politica de acesso, redaction, WAF e evidencia para probes MCP.",
            relation_to_order="0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
        ),
        GeneratedFile(
            path=str(project_root / "matrizes" / "mcp-gateway-access-policy.csv"),
            description="Matriz de checks da politica GPT/MCP Gateway.",
            function="mcp gateway access policy matrix",
            file_type="csv",
            changed_by="mais_humana.mcp_gateway_access_policy",
            change_summary="Criada matriz de regras, status e evidencias de acesso.",
            relation_to_order="0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
        ),
        GeneratedFile(
            path=str(project_root / "ecossistema" / "MCP-GATEWAY-ACCESS-POLICY.md"),
            description="Relatorio humano da politica de acesso GPT/MCP Gateway.",
            function="mcp gateway access policy report",
            file_type="markdown",
            changed_by="mais_humana.mcp_gateway_access_policy",
            change_summary="Criado relatorio de politica para chamada GPT/MCP com evidencia redigida.",
            relation_to_order="0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
        ),
    )


def write_access_policy_artifacts(
    report: McpGatewayAccessPolicyReport,
    project_root: Path,
    *,
    central_platform_folder: Path | None = None,
) -> tuple[GeneratedFile, ...]:
    """Write policy artifacts, tolerating central ACL failures."""

    records = list(access_policy_artifact_records(project_root))
    targets: list[tuple[Path, str, GeneratedFile | None]] = [
        (project_root / "dados" / "mcp-gateway-access-policy.json", json.dumps(report.to_dict(), ensure_ascii=False, indent=2, sort_keys=True), None),
        (project_root / "matrizes" / "mcp-gateway-access-policy.csv", access_policy_csv(report), None),
        (project_root / "ecossistema" / "MCP-GATEWAY-ACCESS-POLICY.md", access_policy_markdown(report), None),
    ]
    if central_platform_folder is not None:
        central_path = central_platform_folder / "reports" / "MCP-GATEWAY-ACCESS-POLICY__RODADA015.md"
        central_record = GeneratedFile(
            path=str(central_path),
            description="Copia central da politica de acesso GPT/MCP Gateway.",
            function="mcp gateway access policy central",
            file_type="markdown",
            changed_by="mais_humana.mcp_gateway_access_policy",
            change_summary="Registrada politica de acesso na pasta central da plataforma 15.",
            relation_to_order="0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
        )
        targets.append((central_path, access_policy_markdown(report), central_record))
    central_failures: list[dict[str, str]] = []
    for path, content, central_record in targets:
        try:
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(redact_sensitive_text(content), encoding="utf-8")
            if central_record is not None:
                records.append(central_record)
        except OSError as exc:
            if central_platform_folder is not None and central_platform_folder in path.parents:
                central_failures.append({"path": str(path), "operation": "write_text", "error": f"{type(exc).__name__}: {exc}"})
                continue
            raise
    if central_failures:
        status_path = project_root / "dados" / "mcp-gateway-access-policy-central-write-status.json"
        status_payload = {
            "generatedAt": utc_now(),
            "centralPlatformFolder": str(central_platform_folder) if central_platform_folder is not None else "",
            "ok": False,
            "failureCount": len(central_failures),
            "failures": central_failures,
            "policy": "falha de escrita central nao aborta artefatos do projeto real",
        }
        status_path.parent.mkdir(parents=True, exist_ok=True)
        status_path.write_text(json.dumps(status_payload, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
        records.append(
            GeneratedFile(
                path=str(status_path),
                description="Status da escrita central da politica de acesso.",
                function="mcp gateway access policy central write status",
                file_type="json",
                changed_by="mais_humana.mcp_gateway_access_policy",
                change_summary="Registrada falha de escrita central sem abortar artefatos do projeto real.",
                relation_to_order="0034_EXECUTIVA__corrigir-acl-escrita-central-e-sql-semantico-plataforma-15",
            )
        )
    return tuple(records)


def run_access_policy_gate(
    *,
    project_root: Path,
    central_platform_folder: Path | None = None,
    publication_gate_json: Path | None = None,
) -> tuple[McpGatewayAccessPolicyReport, tuple[GeneratedFile, ...]]:
    """Build and write the access policy using latest publication-gate probes."""

    gate_path = publication_gate_json or (project_root / "dados" / "mcp-publication-gate-mais-humana.json")
    probes = read_publication_gate_probes(gate_path)
    report = build_access_policy_report(probes=probes)
    records = write_access_policy_artifacts(report, project_root, central_platform_folder=central_platform_folder)
    return report, records

