#!/usr/bin/env bash
set -euo pipefail

ROOT="${AGENTTEAM_MESHX_SERVICE_BOUNDARY_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
cd "$ROOT"

python3 - <<'PY'
import re
import sys
from pathlib import Path

root = Path.cwd()
failures: list[str] = []

retired_meshx_dir = root / "internal/meshx"
if retired_meshx_dir.exists():
    failures.append("internal/meshx: retired transitional namespace must not be recreated")

resource_server_dir = root / "internal/resource/server"
if not resource_server_dir.is_dir():
    failures.append("missing internal/resource/server composition owner")

resource_server_import = '"github.com/tidbcloud/AgentTeam/internal/resource/server"'
for path in sorted((root / "internal").rglob("*.go")):
    rel = path.relative_to(root).as_posix()
    if rel.startswith("internal/meshxruntime/") or rel.startswith("internal/resource/server/"):
        continue
    text = path.read_text(errors="ignore")
    for line_no, line in enumerate(text.splitlines(), start=1):
        if resource_server_import in line:
            failures.append(
                f"{rel}:{line_no}: only internal/meshxruntime may import internal/resource/server"
            )

runtime_file = root / "internal/meshxruntime/runtime.go"
runtime_text = runtime_file.read_text(errors="ignore") if runtime_file.exists() else ""
if resource_server_import not in runtime_text:
    failures.append("internal/meshxruntime must import internal/resource/server")

root_server_file = root / "internal/server/server.go"
root_server_text = root_server_file.read_text(errors="ignore") if root_server_file.exists() else ""
if '"github.com/tidbcloud/AgentTeam/internal/meshxruntime"' not in root_server_text:
    failures.append("internal/server/server.go must compose the resource runtime through internal/meshxruntime")

resource_server_file = resource_server_dir / "server.go"
resource_server_text = resource_server_file.read_text(errors="ignore") if resource_server_file.exists() else ""
if '"github.com/tidbcloud/AgentTeam/internal/router"' not in resource_server_text:
    failures.append("internal/resource/server/server.go must use the canonical internal/router owner")

resource_repos_dir = root / "internal" / "service" / "resource" / "repos"
allowed_resource_repos_service_imports = set()
service_import_re = re.compile(r'"(github\.com/tidbcloud/AgentTeam/internal/service/[^"/]+)(?:/[^"/]*)*"')
if resource_repos_dir.exists():
    for path in sorted(resource_repos_dir.rglob("*.go")):
        rel = path.relative_to(root).as_posix()
        text = path.read_text(errors="ignore")
        for line_no, line in enumerate(text.splitlines(), start=1):
            match = service_import_re.search(line)
            if not match:
                continue
            imported = match.group(1)
            if imported not in allowed_resource_repos_service_imports:
                failures.append(
                    f"{rel}:{line_no}: internal/service/resource/repos must stay domain-agnostic; "
                    f"forbidden service import {imported}"
                )

resource_access_dir = root / "internal" / "service" / "resource" / "access"
if not resource_access_dir.is_dir():
    failures.append("missing internal/service/resource/access owner package")
else:
    for path in sorted(resource_access_dir.rglob("*.go")):
        rel = path.relative_to(root).as_posix()
        text = path.read_text(errors="ignore")
        for line_no, line in enumerate(text.splitlines(), start=1):
            match = service_import_re.search(line)
            if match:
                failures.append(
                    f"{rel}:{line_no}: internal/service/resource/access must stay domain-agnostic; "
                    f"forbidden concrete service import {match.group(1)}"
                )

trusted_identity_dir = root / "internal/identity/trusted"
if not trusted_identity_dir.is_dir():
    failures.append("missing internal/identity/trusted owner package")
else:
    retired_meshx_import_re = re.compile(r'"github\.com/tidbcloud/AgentTeam/internal/meshx(?:/|\")')
    for path in sorted(trusted_identity_dir.rglob("*.go")):
        rel = path.relative_to(root).as_posix()
        text = path.read_text(errors="ignore")
        for line_no, line in enumerate(text.splitlines(), start=1):
            if retired_meshx_import_re.search(line):
                failures.append(
                    f"{rel}:{line_no}: internal/identity/trusted must not import the retired MeshX namespace"
                )

agsbridge_dir = root / "internal/agsbridge"
if not agsbridge_dir.is_dir():
    failures.append("missing internal/agsbridge owner package")
else:
    retired_meshx_import_re = re.compile(r'"github\.com/tidbcloud/AgentTeam/internal/meshx(?:/|\")')
    for path in sorted(agsbridge_dir.rglob("*.go")):
        rel = path.relative_to(root).as_posix()
        text = path.read_text(errors="ignore")
        for line_no, line in enumerate(text.splitlines(), start=1):
            if retired_meshx_import_re.search(line):
                failures.append(
                    f"{rel}:{line_no}: internal/agsbridge must not import the retired MeshX namespace"
                )
    identity_file = agsbridge_dir / "identity.go"
    identity_text = identity_file.read_text(errors="ignore") if identity_file.exists() else ""
    if '"github.com/tidbcloud/AgentTeam/internal/identity/trusted"' not in identity_text:
        failures.append("internal/agsbridge/identity.go must reuse internal/identity/trusted")
    if re.search(r"\btype\s+AgentTeamIdentity\b", identity_text):
        failures.append("internal/agsbridge must not duplicate the trusted AgentTeam identity value")

api_dir = root / "internal" / "api"
if api_dir.is_dir():
    retired_meshx_import_re = re.compile(r'"github\.com/tidbcloud/AgentTeam/internal/meshx(?:/|\")')
    for path in sorted(api_dir.rglob("*.go")):
        rel = path.relative_to(root).as_posix()
        text = path.read_text(errors="ignore")
        for line_no, line in enumerate(text.splitlines(), start=1):
            if retired_meshx_import_re.search(line):
                failures.append(
                    f"{rel}:{line_no}: internal/api must not import the retired MeshX namespace"
                )

# Shared lifecycle owner surface owned by internal/service/resource/repos. Other resource
# service packages must not re-declare these symbols; new use sites import them
# from internal/service/resource/repos. The list is the authoritative shared repository
# contract and helper duplicate-prevention inventory for this guardrail.
SHARED_RESOURCE_REPOS_OWNER_SYMBOLS = {
	"EnsuredRepository",
	"ResourceRepositoryRef",
	"MappingInput",
	"NewResourceRepository",
	"NormalizeEnsuredRepository",
	"RefFromStore",
    "RepositoryIdentity",
    "RepositorySnapshotResolver",
    "RepositoryExistenceChecker",
    "ResourceRepositoryActor",
    "ResourceRepositoryCollaboratorGrant",
    "ResourceRepositoryCollaboratorGrantResult",
    "ResourceRepositoryGrantor",
    "ResourceRepositoryCollaboratorRevoke",
    "ResourceRepositoryRevoker",
    "RepositoryDeleter",
    "AGSRepositoryFeatures",
    "AGSRepositoryEnsureRequest",
    "RepositoryEnsurer",
    "RepositorySyncTokenRequest",
    "RepositorySyncTokenLease",
	"RepositorySyncTokenIssuer",
	"RepositorySyncTokenDefaultProviderHost",
	"RepositorySyncTokenScopeRepositories",
	"RepositorySyncCapabilityRead",
	"RepositorySyncCapabilityClone",
	"RepositorySyncCapabilityWrite",
    "RepositoryStorage",
    "RepositoryOwners",
    "RepositoryNameConflictDetails",
    "RepositoryNameAvailabilityRequest",
    "RepositoryNameAvailability",
    "RepositoryStorageScopePersonal",
    "RepositoryStorageScopeWorkspace",
    "RepositoryNameConflictCode",
    "RepositoryAlreadyExistsCode",
    "RepositoryReservedSuffixCode",
    "RepositoryNameOverrideInvalidCode",
    "RepositoryNameFieldName",
    "RepositoryNameFieldRepoNameOverride",
    "ResolveRepositoryStorage",
    "RepositoryFullName",
    "IdentityFromStore",
    "ResolveRepositoryOwners",
    "CanonicalResourceRepositories",
    "RepositoryNameStem",
    "ResolveRepositoryNameStem",
    "EnsureRepositoryFullNameAvailable",
    "EnsureRepositoryFullNameAvailableForField",
    "RepositoryAlreadyExistsConflict",
    "CheckRepositoryNameAvailability",
    "FirstAvailableRepositoryNameStem",
    "DeleteManagedResourceRepositories",
    "RecordResourceRepositoryLifecycleFailure",
    "ResourceRepositoryFailureRecord",
    "ResourceRepositoryActorFromAgentTeam",
    "RepositoryCommitAuthorFromActor",
}


def _scan_top_level_declarations(text):
    """Return exported top-level names declared in Go source text.

    Tracks single-line func/type/const/var declarations and grouped const/var
    blocks. Method declarations (functions with a receiver) are excluded since
    they attach to existing types and do not introduce a new top-level name.
    """
    declared = set()
    block_kind = None
    for raw in text.splitlines():
        stripped = raw.strip()
        if block_kind is None:
            func_match = re.match(
                r"^func\s+(?:\[[^\]]*\]\s+)?([A-Z]\w*)\s*(?:\(|\b)",
                stripped,
            )
            if func_match and not stripped.startswith("func ("):
                declared.add(func_match.group(1))
                continue
            type_match = re.match(r"^type\s+([A-Z]\w*)\b", stripped)
            if type_match:
                declared.add(type_match.group(1))
                continue
            const_match = re.match(r"^const\s+([A-Z]\w*)\s*=", stripped)
            if const_match:
                declared.add(const_match.group(1))
                continue
            var_match = re.match(r"^var\s+([A-Z]\w*)\s*=", stripped)
            if var_match:
                declared.add(var_match.group(1))
                continue
            if re.match(r"^const\s*\(", stripped):
                block_kind = "const"
                continue
            if re.match(r"^var\s*\(", stripped):
                block_kind = "var"
                continue
        else:
            if stripped == ")":
                block_kind = None
                continue
            block_match = re.match(r"^([A-Z]\w*)\s*=", stripped)
            if block_match:
                declared.add(block_match.group(1))
    return declared


root_service_dir = root / "internal/service"
# Resource domains and AGS publication ports must not re-declare the shared
# resource/repos lifecycle owner surface.
duplicate_scan_targets = [
    (root_service_dir / "automation", "internal/service/automation"),
    (root_service_dir / "automation" / "runtime", "internal/service/automation/runtime"),
    (root_service_dir / "kb", "internal/service/kb"),
    (root_service_dir / "skills", "internal/service/skills"),
    (root_service_dir / "memory", "internal/service/memory"),
    (root / "internal" / "agsbridge" / "wiki", "internal/agsbridge/wiki"),
]
for target_dir, package_label in duplicate_scan_targets:
    if not target_dir.is_dir():
        continue
    for path in sorted(target_dir.glob("*.go")):
        if path.name.endswith("_test.go"):
            continue
        rel = path.relative_to(root).as_posix()
        try:
            text = path.read_text(errors="ignore")
        except OSError:
            continue
        declared = _scan_top_level_declarations(text)
        duplicates = sorted(declared & SHARED_RESOURCE_REPOS_OWNER_SYMBOLS)
        for name in duplicates:
            failures.append(
                f"{rel}: shared resource/repos lifecycle owner symbol "
                f"{name!r} must be imported from internal/service/resource/repos, "
                f"not re-declared in {package_label}"
            )

spec = root / "specs/architecture/meshx-service-boundaries.md"
if not spec.exists():
    failures.append("missing specs/architecture/meshx-service-boundaries.md")
else:
    text = spec.read_text(errors="ignore")
    required = [
        "Canonical path: `specs/architecture/meshx-service-boundaries.md`",
        "`internal/resource/server` owns resource-service composition:",
        "`internal/server → internal/meshxruntime → internal/resource/server → internal/router`",
        "The retired MeshX transitional namespace must remain absent",
        "`internal/api` must not import the retired MeshX namespace",
        "`internal/service/resource/repos` owns AGS-backed resource repository lifecycle primitives",
        "`internal/service/automation`, `internal/service/kb`, `internal/service/memory`, and `internal/agsbridge/wiki`",
        "`internal/service/memory` consumes those lifecycle",
        "`internal/service/resource/access` owns the domain-agnostic access-change outbox helper",
        "`internal/identity/trusted` owns the AgentTeam-verified JWT identity value",
        "`internal/agsbridge` owns trusted embedded-AGS context markers",
    ]
    for needle in required:
        if needle not in text:
            failures.append(f"specs/architecture/meshx-service-boundaries.md: missing {needle!r}")

if failures:
    for failure in failures:
        print(f"FAIL: {failure}", file=sys.stderr)
    print(f"\nMeshX service boundary check failed with {len(failures)} issue(s).", file=sys.stderr)
    sys.exit(1)

print("MeshX service boundary check passed.")
PY
