"""Canonical identity graph for repository, MCP, and central aliases.

The Mais Humana platform now has an institutional canonical name:
``tudo-para-ia-mais-humana-platform``.  The physical repository is still
materialized as the historical no-suffix folder in this workspace, and other
platforms also carry ``platform``/``plataform`` history.  This module makes
that identity policy executable: it builds a graph of accepted identifiers,
validates MCP payloads, and writes auditable artifacts for the central dossier.
"""

from __future__ import annotations

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

from .identity_policy import (
    CANONICAL_COMPATIBILITY_RULE,
    CANONICAL_DECISION_SOURCE,
    CANONICAL_PROJECT_ID,
    CENTRAL_FOLDER_NAME,
    CURRENT_PROJECT_ID,
    LEGACY_PLATAFORM_ALIAS,
    MCP_CONTROL_PLANE_ID,
)
from .models import GeneratedFile, as_plain_data, merge_unique, slugify, utc_now
from .repository_mesh import RepositoryTarget, default_repository_targets, stable_digest
from .repository_mesh_naming import plataform_to_platform, platform_to_plataform
from .storage import connect, upsert_files


class CanonicalAliasKind(str, Enum):
    """Kinds of identifiers accepted by the graph."""

    CANONICAL_PROJECT_ID = "canonical_project_id"
    CURRENT_PROJECT_ID = "current_project_id"
    LEGACY_PROJECT_ID = "legacy_project_id"
    SPELLING_VARIANT = "spelling_variant"
    CENTRAL_FOLDER = "central_folder"
    REMOTE_URL = "remote_url"
    GITEA_REPOSITORY = "gitea_repository"


class IdentityIssueSeverity(str, Enum):
    """Severity for identity validation issues."""

    INFO = "info"
    WARNING = "warning"
    BLOCKER = "blocker"


CANONICAL_REQUIRED_FIELDS = {
    "canonicalProjectId",
    "ownerPlatformId",
    "owner_platform_id",
}

COMPATIBILITY_IDENTIFIER_FIELDS = {
    "projectId",
    "project_id",
    "currentProjectId",
    "current_project_id",
    "platformId",
    "platform_id",
    "origin",
    "destination",
    "targetPlatformId",
    "target_platform_id",
    "repositoryName",
    "repoName",
    "repo_name",
}

REMOTE_IDENTIFIER_FIELDS = {
    "repoRemote",
    "remoteOrigin",
    "remote_origin",
    "originRemote",
}

CENTRAL_IDENTIFIER_FIELDS = {
    "centralFolder",
    "central_folder",
    "centralPlatformFolder",
    "central_platform_folder",
}

MCP_ADMIN_OPERATIONS: tuple[tuple[str, str], ...] = (
    ("consulta", "mcp.admin.readonly"),
    ("diagnostico", "mcp.admin.diagnostic"),
    ("acao", "mcp.admin.action.request"),
    ("auditoria", "mcp.admin.audit"),
    ("explicacao", "mcp.admin.explain"),
)

IDENTITY_TRANSIT_FIELDS: tuple[str, ...] = (
    "origin",
    "destination",
    "ownerPlatformId",
    "targetPlatformId",
    "projectId",
    "canonicalProjectId",
    "currentProjectId",
    "repositoryName",
    "centralFolder",
    "repoRemote",
    "giteaRepository",
)


@dataclass(frozen=True, slots=True)
class CanonicalAlias:
    """One accepted alias or locator for a platform identity."""

    identifier: str
    kind: CanonicalAliasKind
    accepted: bool
    canonical: bool
    reason: str
    required_action: str = ""

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


@dataclass(frozen=True, slots=True)
class CanonicalIdentityRecord:
    """Canonical identity for one managed repository or platform."""

    platform_id: str
    canonical_project_id: str
    current_project_id: str
    central_folder: str
    gitea_repo: str
    expected_remote_url: str
    owner_platform_id: str
    aliases: tuple[CanonicalAlias, ...]
    decision_status: str
    decision_source: str
    compatibility_rule: str
    migration_safe_now: bool
    notes: tuple[str, ...] = ()

    @property
    def accepted_identifiers(self) -> tuple[str, ...]:
        return merge_unique(alias.identifier for alias in self.aliases if alias.accepted)

    @property
    def canonical_aliases(self) -> tuple[CanonicalAlias, ...]:
        return tuple(alias for alias in self.aliases if alias.canonical)

    def alias_for(self, identifier: str) -> CanonicalAlias | None:
        normalized = normalize_identifier(identifier)
        for alias in self.aliases:
            if normalize_identifier(alias.identifier) == normalized:
                return alias
        return None

    def canonicalize(self, identifier: str) -> str:
        alias = self.alias_for(identifier)
        if alias and alias.accepted:
            return self.canonical_project_id
        return identifier

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


@dataclass(frozen=True, slots=True)
class CanonicalIdentityAcceptanceCase:
    """Generated acceptance case for MCP transit and payload identifiers."""

    case_id: str
    platform_id: str
    operation: str
    permission_scope: str
    field_name: str
    candidate_value: str
    canonical_project_id: str
    accepted: bool
    status: str
    decision_reason: str
    required_action: str
    mcp_transit_required: bool
    direct_platform_bypass_blocked: bool

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


@dataclass(frozen=True, slots=True)
class IdentityValidationIssue:
    """One validation issue found in a payload."""

    field_name: str
    value: str
    severity: IdentityIssueSeverity
    message: str
    canonical_project_id: str = ""
    required_action: str = ""

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


@dataclass(frozen=True, slots=True)
class IdentityValidationResult:
    """Result of validating identifiers present in one payload."""

    ok: bool
    canonical_project_ids: tuple[str, ...]
    accepted_aliases: tuple[str, ...]
    issues: tuple[IdentityValidationIssue, ...]

    @property
    def blockers(self) -> tuple[IdentityValidationIssue, ...]:
        return tuple(issue for issue in self.issues if issue.severity == IdentityIssueSeverity.BLOCKER)

    @property
    def warnings(self) -> tuple[IdentityValidationIssue, ...]:
        return tuple(issue for issue in self.issues if issue.severity == IdentityIssueSeverity.WARNING)

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


@dataclass(frozen=True, slots=True)
class CanonicalIdentityGraph:
    """Full identity graph and generated acceptance cases."""

    graph_id: str
    generated_at: str
    records: tuple[CanonicalIdentityRecord, ...]
    acceptance_cases: tuple[CanonicalIdentityAcceptanceCase, ...]
    decision_source: str
    compatibility_rule: str

    @property
    def records_count(self) -> int:
        return len(self.records)

    @property
    def aliases_count(self) -> int:
        return sum(len(record.aliases) for record in self.records)

    @property
    def accepted_cases_count(self) -> int:
        return sum(1 for case in self.acceptance_cases if case.accepted)

    @property
    def blocked_cases_count(self) -> int:
        return sum(1 for case in self.acceptance_cases if not case.accepted)

    def record_for(self, identifier: str) -> CanonicalIdentityRecord | None:
        normalized = normalize_identifier(identifier)
        for record in self.records:
            if normalize_identifier(record.canonical_project_id) == normalized:
                return record
            if normalize_identifier(record.current_project_id) == normalized:
                return record
            if normalize_identifier(record.central_folder) == normalized:
                return record
            if normalize_identifier(record.expected_remote_url) == normalized:
                return record
            if normalize_identifier(record.gitea_repo) == normalized:
                return record
            if record.alias_for(identifier):
                return record
        return None

    def canonicalize(self, identifier: str) -> str:
        record = self.record_for(identifier)
        if record is None:
            return identifier
        return record.canonicalize(identifier)

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


def normalize_identifier(value: str) -> str:
    """Normalize path-ish, URL-ish, and slug-ish identifiers for lookup."""

    text = str(value or "").strip().replace("\\", "/").rstrip("/")
    if text.endswith(".git"):
        text = text[: -len(".git")]
    return text.lower()


def platform_id_from_repo_name(name: str) -> str:
    text = str(name).strip()
    if text.startswith("tudo-para-ia-"):
        text = text[len("tudo-para-ia-") :]
    for suffix in ("-platform", "-plataform"):
        if text.endswith(suffix):
            text = text[: -len(suffix)]
            break
    return text.replace("-", "_")


def _alias(identifier: str, kind: CanonicalAliasKind, canonical: bool, reason: str, required_action: str = "") -> CanonicalAlias:
    return CanonicalAlias(
        identifier=identifier,
        kind=kind,
        accepted=True,
        canonical=canonical,
        reason=reason,
        required_action=required_action,
    )


def aliases_for_target(target: RepositoryTarget) -> tuple[CanonicalAlias, ...]:
    """Build accepted aliases for one repository target."""

    canonical_id = target.canonical_name or target.expected_local_name
    raw_values: list[CanonicalAlias] = [
        _alias(
            canonical_id,
            CanonicalAliasKind.CANONICAL_PROJECT_ID,
            True,
            "identificador canonico do repositorio/plataforma",
        ),
        _alias(
            target.expected_local_name,
            CanonicalAliasKind.CURRENT_PROJECT_ID,
            target.expected_local_name == canonical_id,
            "nome local esperado pelo inventario de sincronizacao",
            "usar canonico em ownerPlatformId quando houver divergencia",
        ),
        _alias(
            target.declared_name,
            CanonicalAliasKind.CURRENT_PROJECT_ID,
            target.declared_name == canonical_id,
            "nome declarado pela ordem permanente de sincronizacao",
            "registrar divergencia se diferente do canonico",
        ),
        _alias(
            target.central_folder,
            CanonicalAliasKind.CENTRAL_FOLDER,
            False,
            "pasta gerencial da central de ordem de servico",
            "nao usar pasta central como ownerPlatformId",
        ),
        _alias(
            target.gitea_repo,
            CanonicalAliasKind.GITEA_REPOSITORY,
            False,
            "repositorio Gitea esperado",
            "normalizar para remote URL antes de publicar evidencia",
        ),
        _alias(
            target.expected_remote_url,
            CanonicalAliasKind.REMOTE_URL,
            False,
            "remote HTTPS esperado",
            "validar credencial Git antes de sincronizar",
        ),
    ]
    for alias in target.aliases:
        raw_values.append(
            _alias(
                alias,
                CanonicalAliasKind.LEGACY_PROJECT_ID,
                alias == canonical_id,
                "alias historico autorizado para compatibilidade",
                "preservar alias ate migracao Git/MCP coordenada",
            )
        )
    for variant in (platform_to_plataform(canonical_id), plataform_to_platform(canonical_id)):
        if variant != canonical_id:
            raw_values.append(
                _alias(
                    variant,
                    CanonicalAliasKind.SPELLING_VARIANT,
                    False,
                    "variante platform/plataform reconhecida para evitar repositorio duplicado",
                    "registrar como alias e nao criar repositorio paralelo",
                )
            )
    by_identifier: dict[str, CanonicalAlias] = {}
    for item in raw_values:
        key = normalize_identifier(item.identifier)
        if key not in by_identifier or item.canonical:
            by_identifier[key] = item
    return tuple(by_identifier.values())


def record_for_target(target: RepositoryTarget) -> CanonicalIdentityRecord:
    canonical_id = target.canonical_name or target.expected_local_name
    platform_id = platform_id_from_repo_name(canonical_id)
    if canonical_id == CANONICAL_PROJECT_ID:
        platform_id = "mais_humana"
    current_id = CURRENT_PROJECT_ID if canonical_id == CANONICAL_PROJECT_ID else target.expected_local_name
    decision_source = CANONICAL_DECISION_SOURCE if canonical_id == CANONICAL_PROJECT_ID else "000_sincronizacao-dos-espelhos.md"
    compatibility_rule = (
        CANONICAL_COMPATIBILITY_RULE
        if canonical_id == CANONICAL_PROJECT_ID
        else "Identidade aceita conforme inventario permanente; variantes platform/plataform sao aliases ate reconciliacao segura."
    )
    migration_safe_now = bool(canonical_id == target.expected_local_name and not target.requires_nominal_reconciliation)
    return CanonicalIdentityRecord(
        platform_id=platform_id,
        canonical_project_id=canonical_id,
        current_project_id=current_id,
        central_folder=target.central_folder,
        gitea_repo=target.gitea_repo,
        expected_remote_url=target.expected_remote_url,
        owner_platform_id=canonical_id,
        aliases=aliases_for_target(target),
        decision_status="approved" if canonical_id == CANONICAL_PROJECT_ID else "inventory_declared",
        decision_source=decision_source,
        compatibility_rule=compatibility_rule,
        migration_safe_now=migration_safe_now,
        notes=target.notes,
    )


def build_identity_records(targets: Sequence[RepositoryTarget] | None = None) -> tuple[CanonicalIdentityRecord, ...]:
    """Build identity records from repository targets."""

    return tuple(record_for_target(target) for target in (targets or default_repository_targets()))


def build_acceptance_cases(records: Sequence[CanonicalIdentityRecord]) -> tuple[CanonicalIdentityAcceptanceCase, ...]:
    """Build exhaustive MCP transit acceptance cases for records and aliases."""

    cases: list[CanonicalIdentityAcceptanceCase] = []
    for record in records:
        candidates = tuple(record.aliases)
        for operation, permission in MCP_ADMIN_OPERATIONS:
            for field in IDENTITY_TRANSIT_FIELDS:
                for alias in candidates:
                    candidate = alias.identifier
                    accepted = alias.accepted
                    canonical_field = field in CANONICAL_REQUIRED_FIELDS
                    status = "canonical" if alias.canonical else "compatibility_alias"
                    required_action = alias.required_action
                    if canonical_field and candidate != record.canonical_project_id:
                        status = "canonical_field_requires_rewrite"
                        required_action = "reescrever campo canonico para canonicalProjectId/ownerPlatformId antes de persistir"
                    case_seed = {
                        "platform": record.platform_id,
                        "operation": operation,
                        "field": field,
                        "candidate": candidate,
                        "canonical": record.canonical_project_id,
                    }
                    cases.append(
                        CanonicalIdentityAcceptanceCase(
                            case_id=f"identity-{stable_digest(case_seed, 20)}",
                            platform_id=record.platform_id,
                            operation=operation,
                            permission_scope=permission,
                            field_name=field,
                            candidate_value=candidate,
                            canonical_project_id=record.canonical_project_id,
                            accepted=accepted,
                            status=status,
                            decision_reason=alias.reason,
                            required_action=required_action,
                            mcp_transit_required=True,
                            direct_platform_bypass_blocked=True,
                        )
                    )
    return tuple(cases)


def _generated_records_and_cases() -> tuple[tuple[CanonicalIdentityRecord, ...], tuple[CanonicalIdentityAcceptanceCase, ...]] | None:
    try:
        from .generated_canonical_identity_registry import iter_acceptance_cases, iter_records
    except ImportError:
        return None
    return tuple(iter_records()), tuple(iter_acceptance_cases())


def build_identity_graph(*, use_generated: bool = True) -> CanonicalIdentityGraph:
    """Build the graph, preferring the generated registry when present."""

    generated = _generated_records_and_cases() if use_generated else None
    if generated is None:
        records = build_identity_records()
        cases = build_acceptance_cases(records)
    else:
        records, cases = generated
    seed = {
        "records": [record.canonical_project_id for record in records],
        "aliases": sum(len(record.aliases) for record in records),
        "cases": len(cases),
        "decision": CANONICAL_DECISION_SOURCE,
    }
    return CanonicalIdentityGraph(
        graph_id=f"canonical-identity-{stable_digest(seed, 16)}",
        generated_at=utc_now(),
        records=records,
        acceptance_cases=cases,
        decision_source=CANONICAL_DECISION_SOURCE,
        compatibility_rule=CANONICAL_COMPATIBILITY_RULE,
    )


def _iter_payload_items(payload: Mapping[str, Any], prefix: str = "") -> Iterable[tuple[str, str]]:
    for key, value in payload.items():
        field = f"{prefix}.{key}" if prefix else str(key)
        if isinstance(value, Mapping):
            yield from _iter_payload_items(value, field)
        elif isinstance(value, (list, tuple)):
            for index, item in enumerate(value):
                item_field = f"{field}[{index}]"
                if isinstance(item, Mapping):
                    yield from _iter_payload_items(item, item_field)
                elif isinstance(item, str):
                    yield item_field, item
        elif isinstance(value, str):
            yield field, value


def _base_field_name(field_name: str) -> str:
    text = field_name.rsplit(".", 1)[-1]
    if "[" in text:
        text = text.split("[", 1)[0]
    return text


def _field_is_identity_relevant(field_name: str) -> bool:
    base = _base_field_name(field_name)
    return (
        base in CANONICAL_REQUIRED_FIELDS
        or base in COMPATIBILITY_IDENTIFIER_FIELDS
        or base in REMOTE_IDENTIFIER_FIELDS
        or base in CENTRAL_IDENTIFIER_FIELDS
    )


def validate_identity_payload(
    payload: Mapping[str, Any],
    *,
    graph: CanonicalIdentityGraph | None = None,
) -> IdentityValidationResult:
    """Validate identity fields in an MCP or central payload."""

    identity_graph = graph or build_identity_graph()
    issues: list[IdentityValidationIssue] = []
    accepted_aliases: list[str] = []
    canonical_ids: list[str] = []
    for field_name, value in _iter_payload_items(payload):
        if not _field_is_identity_relevant(field_name):
            continue
        base = _base_field_name(field_name)
        record = identity_graph.record_for(value)
        if record is None:
            issues.append(
                IdentityValidationIssue(
                    field_name=field_name,
                    value=value,
                    severity=IdentityIssueSeverity.BLOCKER,
                    message="identificador nao reconhecido no grafo canonico",
                    required_action="registrar alias institucional ou corrigir payload antes de publicar",
                )
            )
            continue
        canonical_ids.append(record.canonical_project_id)
        alias = record.alias_for(value)
        if alias and not alias.canonical:
            accepted_aliases.append(alias.identifier)
        if base in CANONICAL_REQUIRED_FIELDS and value != record.canonical_project_id:
            issues.append(
                IdentityValidationIssue(
                    field_name=field_name,
                    value=value,
                    severity=IdentityIssueSeverity.WARNING,
                    message="campo canonico recebeu alias aceito; reescrita recomendada antes de publicar estado novo",
                    canonical_project_id=record.canonical_project_id,
                    required_action="usar canonical_project_id no owner/canonical field e manter alias apenas como compatibilidade",
                )
            )
        elif alias and not alias.canonical:
            issues.append(
                IdentityValidationIssue(
                    field_name=field_name,
                    value=value,
                    severity=IdentityIssueSeverity.INFO,
                    message="alias aceito por politica de compatibilidade",
                    canonical_project_id=record.canonical_project_id,
                    required_action=alias.required_action,
                )
            )
    blockers = tuple(issue for issue in issues if issue.severity == IdentityIssueSeverity.BLOCKER)
    return IdentityValidationResult(
        ok=not blockers,
        canonical_project_ids=merge_unique(canonical_ids),
        accepted_aliases=merge_unique(accepted_aliases),
        issues=tuple(issues),
    )


def identity_graph_payload(graph: CanonicalIdentityGraph, *, limit_cases: int = 120) -> dict[str, Any]:
    """Return a compact JSON-safe identity graph payload."""

    return {
        "graphId": graph.graph_id,
        "generatedAt": graph.generated_at,
        "recordsCount": graph.records_count,
        "aliasesCount": graph.aliases_count,
        "acceptanceCasesCount": len(graph.acceptance_cases),
        "acceptedCasesCount": graph.accepted_cases_count,
        "blockedCasesCount": graph.blocked_cases_count,
        "decisionSource": graph.decision_source,
        "compatibilityRule": graph.compatibility_rule,
        "controlPlaneId": MCP_CONTROL_PLANE_ID,
        "maisHumanaCanonicalProjectId": CANONICAL_PROJECT_ID,
        "maisHumanaCurrentProjectId": CURRENT_PROJECT_ID,
        "maisHumanaLegacyAlias": LEGACY_PLATAFORM_ALIAS,
        "maisHumanaCentralFolder": CENTRAL_FOLDER_NAME,
        "records": [record.to_dict() for record in graph.records],
        "acceptanceCasesSample": [case.to_dict() for case in graph.acceptance_cases[:limit_cases]],
    }


def identity_graph_rows(graph: CanonicalIdentityGraph) -> list[list[str]]:
    rows = [
        [
            "platform_id",
            "canonical_project_id",
            "current_project_id",
            "central_folder",
            "alias",
            "alias_kind",
            "alias_canonical",
            "decision_status",
            "migration_safe_now",
            "required_action",
        ]
    ]
    for record in graph.records:
        for alias in record.aliases:
            rows.append(
                [
                    record.platform_id,
                    record.canonical_project_id,
                    record.current_project_id,
                    record.central_folder,
                    alias.identifier,
                    alias.kind.value,
                    "yes" if alias.canonical else "no",
                    record.decision_status,
                    "yes" if record.migration_safe_now else "no",
                    alias.required_action,
                ]
            )
    return rows


def identity_acceptance_rows(graph: CanonicalIdentityGraph) -> list[list[str]]:
    rows = [
        [
            "case_id",
            "platform_id",
            "operation",
            "permission_scope",
            "field_name",
            "candidate_value",
            "canonical_project_id",
            "accepted",
            "status",
            "required_action",
        ]
    ]
    for case in graph.acceptance_cases:
        rows.append(
            [
                case.case_id,
                case.platform_id,
                case.operation,
                case.permission_scope,
                case.field_name,
                case.candidate_value,
                case.canonical_project_id,
                "yes" if case.accepted else "no",
                case.status,
                case.required_action,
            ]
        )
    return rows


def rows_to_csv(rows: Sequence[Sequence[str]]) -> str:
    buffer = io.StringIO()
    writer = csv.writer(buffer, lineterminator="\n")
    writer.writerows(rows)
    return buffer.getvalue()


def identity_graph_markdown(graph: CanonicalIdentityGraph) -> str:
    lines = [
        "# Canonical Identity Graph",
        "",
        f"- graph_id: `{graph.graph_id}`",
        f"- generated_at: `{graph.generated_at}`",
        f"- records: `{graph.records_count}`",
        f"- aliases: `{graph.aliases_count}`",
        f"- acceptance_cases: `{len(graph.acceptance_cases)}`",
        f"- accepted_cases: `{graph.accepted_cases_count}`",
        f"- blocked_cases: `{graph.blocked_cases_count}`",
        f"- decision_source: `{graph.decision_source}`",
        f"- control_plane: `{MCP_CONTROL_PLANE_ID}`",
        "",
        "## Regra canonica Mais Humana",
        "",
        f"- canonico: `{CANONICAL_PROJECT_ID}`",
        f"- repo_local_historico: `{CURRENT_PROJECT_ID}`",
        f"- alias_plataform: `{LEGACY_PLATAFORM_ALIAS}`",
        f"- pasta_central: `{CENTRAL_FOLDER_NAME}`",
        f"- regra: {CANONICAL_COMPATIBILITY_RULE}",
        "",
        "## Plataformas",
        "",
    ]
    for record in sorted(graph.records, key=lambda item: item.platform_id):
        lines.extend(
            [
                f"### {record.platform_id}",
                "",
                f"- canonical_project_id: `{record.canonical_project_id}`",
                f"- current_project_id: `{record.current_project_id}`",
                f"- central_folder: `{record.central_folder}`",
                f"- remote: `{record.expected_remote_url}`",
                f"- migration_safe_now: `{record.migration_safe_now}`",
                f"- aliases: `{', '.join(record.accepted_identifiers)}`",
                "",
            ]
        )
    lines.extend(
        [
            "## MCP transit",
            "",
            "- Todo payload interplataforma deve manter origin, destination, tool, payload, actor, permission, result, traceId, auditId e timestamp.",
            "- Campos ownerPlatformId/canonicalProjectId devem usar o canonical_project_id; aliases sao aceitos apenas como compatibilidade rastreavel.",
            "- Bypass direto da plataforma permanece bloqueado: a administracao passa pelo MCPs Internos.",
        ]
    )
    return "\n".join(lines).strip() + "\n"


def identity_generated_records(project_root: Path, central_platform_folder: Path | None = None) -> tuple[GeneratedFile, ...]:
    relation = "0035_EXECUTIVA__reconciliar-nome-canonico-real-alias-platform"
    paths = (
        ("dados/canonical-identity-graph.json", "Grafo canonico de identidades e aliases.", "canonical identity graph", "json"),
        ("matrizes/canonical-identity-graph.csv", "Matriz de aliases por plataforma.", "canonical identity matrix", "csv"),
        ("matrizes/canonical-identity-acceptance-cases.csv", "Casos MCP de aceitacao de aliases.", "canonical identity acceptance", "csv"),
        ("ecossistema/CANONICAL-IDENTITY-GRAPH.md", "Relatorio humano do grafo canonico.", "canonical identity report", "markdown"),
    )
    records = [
        GeneratedFile(
            path=str(project_root / relative),
            description=description,
            function=function,
            file_type=file_type,
            changed_by="mais_humana.canonical_identity",
            change_summary="Criado ou atualizado grafo canonico de nomes, aliases e casos MCP.",
            relation_to_order=relation,
        )
        for relative, description, function, file_type in paths
    ]
    if central_platform_folder is not None:
        records.append(
            GeneratedFile(
                path=str(central_platform_folder / "reports" / "EXECUTADO__canonical-identity-graph.md"),
                description="Copia central do grafo canonico de identidade.",
                function="canonical identity central report",
                file_type="markdown",
                changed_by="mais_humana.canonical_identity",
                change_summary="Registrada decisao canonica -platform e aliases no dossie central.",
                relation_to_order=relation,
            )
        )
    return tuple(records)


def write_identity_graph_artifacts(
    graph: CanonicalIdentityGraph,
    project_root: Path,
    *,
    central_platform_folder: Path | None = None,
) -> tuple[GeneratedFile, ...]:
    targets: list[tuple[Path, str]] = [
        (project_root / "dados" / "canonical-identity-graph.json", json.dumps(identity_graph_payload(graph), ensure_ascii=False, indent=2, sort_keys=True)),
        (project_root / "matrizes" / "canonical-identity-graph.csv", rows_to_csv(identity_graph_rows(graph))),
        (project_root / "matrizes" / "canonical-identity-acceptance-cases.csv", rows_to_csv(identity_acceptance_rows(graph))),
        (project_root / "ecossistema" / "CANONICAL-IDENTITY-GRAPH.md", identity_graph_markdown(graph)),
    ]
    records = list(identity_generated_records(project_root, None))
    central_failures: list[dict[str, str]] = []
    central_record: GeneratedFile | None = None
    central_target: Path | None = None
    if central_platform_folder is not None:
        central_target = central_platform_folder / "reports" / "EXECUTADO__canonical-identity-graph.md"
        central_record = GeneratedFile(
            path=str(central_target),
            description="Copia central do grafo canonico de identidade.",
            function="canonical identity central report",
            file_type="markdown",
            changed_by="mais_humana.canonical_identity",
            change_summary="Registrada decisao canonica -platform e aliases no dossie central.",
            relation_to_order="0035_EXECUTIVA__reconciliar-nome-canonico-real-alias-platform",
        )
        targets.append((central_target, identity_graph_markdown(graph)))
    for path, content in targets:
        try:
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(content, encoding="utf-8")
            if central_target is not None and path == central_target and 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), "error": f"{type(exc).__name__}: {exc}"})
                continue
            raise
    if central_failures:
        status_path = project_root / "dados" / "canonical-identity-central-write-status.json"
        status_path.write_text(
            json.dumps(
                {
                    "generatedAt": utc_now(),
                    "centralPlatformFolder": str(central_platform_folder),
                    "ok": False,
                    "failures": central_failures,
                },
                ensure_ascii=False,
                indent=2,
                sort_keys=True,
            ),
            encoding="utf-8",
        )
        records.append(
            GeneratedFile(
                path=str(status_path),
                description="Status da escrita central do grafo canonico.",
                function="canonical identity central write status",
                file_type="json",
                changed_by="mais_humana.canonical_identity",
                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 _semantic_status_record(project_root: Path) -> GeneratedFile:
    return GeneratedFile(
        path=str(project_root / "dados" / "canonical-identity-semantic-write-status.json"),
        description="Status da escrita semantica local do grafo canonico.",
        function="canonical identity local semantic write status",
        file_type="json",
        changed_by="mais_humana.canonical_identity",
        change_summary="Registrado resultado da atualizacao do controle-semantico.sqlite local.",
        relation_to_order="0035_EXECUTIVA__reconciliar-nome-canonico-real-alias-platform",
    )


def write_local_semantic_records(project_root: Path, records: Sequence[GeneratedFile]) -> tuple[GeneratedFile, ...]:
    sqlite_path = project_root / "controle-semantico.sqlite"
    status_record = _semantic_status_record(project_root)
    status_path = Path(status_record.path)
    tracked_records = tuple(records) + (status_record,)
    error = ""
    ok = False
    try:
        with connect(sqlite_path) as conn:
            upsert_files(conn, tracked_records)
        ok = True
    except Exception as exc:
        error = f"{type(exc).__name__}: {exc}"
    status_path.parent.mkdir(parents=True, exist_ok=True)
    status_path.write_text(
        json.dumps(
            {
                "generatedAt": utc_now(),
                "ok": ok,
                "error": error,
                "sqlitePath": str(sqlite_path),
                "filesCount": len(tracked_records),
            },
            ensure_ascii=False,
            indent=2,
            sort_keys=True,
        ),
        encoding="utf-8",
    )
    if ok:
        with connect(sqlite_path) as conn:
            upsert_files(conn, (status_record,))
    return tracked_records


def run_canonical_identity_graph(
    *,
    project_root: Path,
    central_platform_folder: Path | None = None,
    use_generated: bool = True,
) -> tuple[CanonicalIdentityGraph, tuple[GeneratedFile, ...]]:
    graph = build_identity_graph(use_generated=use_generated)
    records = write_identity_graph_artifacts(graph, project_root, central_platform_folder=central_platform_folder)
    semantic_records = write_local_semantic_records(project_root, records)
    return graph, semantic_records
