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

ROOT="${AGENTTEAM_SERVICE_ARCH_GUARDRAIL_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] = []


def production_go_files(directory: str) -> list[Path]:
    base = root / directory
    if not base.is_dir():
        failures.append(f"missing architecture owner directory: {directory}")
        return []
    return sorted(path for path in base.rglob("*.go") if not path.name.endswith("_test.go"))


def rel(path: Path) -> str:
    return path.relative_to(root).as_posix()


def read(path: Path) -> str:
    try:
        return path.read_text(errors="ignore")
    except OSError as exc:
        failures.append(f"cannot read {rel(path)}: {exc}")
        return ""


def line_number(text: str, offset: int) -> int:
    return text.count("\n", 0, offset) + 1


def reject_regex(path: Path, pattern: str, message: str, flags: int = 0) -> None:
    text = read(path)
    for match in re.finditer(pattern, text, flags):
        failures.append(f"{rel(path)}:{line_number(text, match.start())}: {message}")


def require_regex(path: str, pattern: str, message: str, flags: int = 0) -> None:
    file_path = root / path
    if not file_path.is_file():
        failures.append(f"{message}: missing {path}")
        return
    if not re.search(pattern, read(file_path), flags):
        failures.append(f"{message}: expected pattern not found in {path}")


def function_body(path: str, name: str) -> str:
    file_path = root / path
    if not file_path.is_file():
        failures.append(f"missing source contract owner: {path}")
        return ""
    text = read(file_path)
    match = re.search(
        rf"(?ms)^func\s+{re.escape(name)}\s*\(.*?^\}}",
        text,
    )
    if not match:
        failures.append(f"missing source contract function {name} in {path}")
        return ""
    return match.group(0)


def require_tokens(body: str, owner: str, required: set[str], forbidden: set[str]) -> None:
    for token in sorted(required):
        if token not in body:
            failures.append(f"{owner} must use {token}")
    for token in sorted(forbidden):
        if token in body:
            failures.append(f"{owner} must not use {token}")


# Service Actor and provider-event normalization are lower-level owners. They
# must not depend on App, Schedule, Automation, or service orchestration code.
lower_level_roots = ["internal/store/serviceactor", "internal/core/webhook"]
for directory in lower_level_roots:
    for path in production_go_files(directory):
        reject_regex(
            path,
            r'"github\.com/tidbcloud/AgentTeam/internal/(?:store/app|schedule|automation|service)(?:/|\")',
            "lower-level Service Actor/webhook owner must not import App, Schedule, Automation, or service orchestration",
        )


# The Automation core is domain-neutral. Product/store dependencies belong in
# its service composition layer, not the core DSL/blob packages.
for path in production_go_files("internal/automation"):
    reject_regex(
        path,
        r'"github\.com/tidbcloud/AgentTeam/internal/(?:store|schedule|service)(?:/|\")',
        "Automation core must not import store, Schedule, or service packages",
    )


# App currently owns the Service Event implementation, including files named
# for pull-request subscriptions, provider-watch facades, deliveries, and App
# runtime. Scan the complete production package instead of inferring owner
# membership from a file-name prefix.
app_files = production_go_files("internal/store/app")
app_automation_import_owners = {
    # Presentation adapter for Schedule-authored App messages.
    "internal/store/app/schedule_message_source.go",
}
for path in app_files:
    reject_regex(
        path,
        r'"github\.com/tidbcloud/AgentTeam/internal/(?:automation|trigger|service/automation)(?:/|\")|'
        r'"github\.com/tidbcloud/AgentTeam/internal/store/automation(?:/|\")',
        "App-owned Service Event path must not import Automation or Trigger",
    )
    if rel(path) not in app_automation_import_owners:
        reject_regex(
            path,
            r'"github\.com/tidbcloud/AgentTeam/internal/(?:schedule|store/schedule)(?:/|\")',
            "App-owned Service Event path may import Schedule only in the named presentation adapter",
        )
    reject_regex(
        path,
        r"\btrigger_(?:definitions|runs)\b",
        "legacy trigger tables are retired from App-owned Service Event runtime",
        re.IGNORECASE,
    )

for owner in sorted(app_automation_import_owners):
    owner_path = root / owner
    if not owner_path.is_file() or not re.search(
        r'"github\.com/tidbcloud/AgentTeam/internal/(?:schedule|store/schedule)(?:/|\")',
        read(owner_path) if owner_path.is_file() else "",
    ):
        failures.append(f"stale Schedule import owner exception: {owner}")

# Schedule management uses the canonical schedule store while preserving the
# stable trigger-shaped facade. Native Schedule files remain Automation-backed.
schedule_compat_owner = "internal/schedule/manage/manage_schedule.go"
schedule_files = production_go_files("internal/schedule")
for path in schedule_files:
    if rel(path) == schedule_compat_owner:
        continue
    reject_regex(
        path,
        r'"github\.com/tidbcloud/AgentTeam/internal/store/app(?:/|\")|'
        r'"github\.com/tidbcloud/AgentTeam/internal/service/serviceevent(?:/|\")',
        "native Schedule code must not import App or Service Event",
    )
    reject_regex(
        path,
        r"\bservice_(?:event_subscriptions|event_deliveries|webhook_receipts)\b",
        "native Schedule code must not read Service Event tables",
        re.IGNORECASE,
    )


# Visible contribution provenance is the durable boundary between the domains:
# provider events reference service_event_delivery; schedules reference
# automation_run. Symbol-level checks tolerate refactors and formatting changes
# while rejecting cross-wiring of the two ledgers.
source_path = "internal/store/message/contribution_source.go"
source_constants = {
    "ContributionSourceKindProviderEvent": "provider_event",
    "ContributionSourceKindScheduleRun": "schedule_run",
    "ContributionSourceRefTypeServiceEventDelivery": "service_event_delivery",
    "ContributionSourceRefTypeAutomationRun": "automation_run",
}
for symbol, value in source_constants.items():
    require_regex(
        source_path,
        rf'\b{re.escape(symbol)}\s*=\s*"{re.escape(value)}"',
        f"contribution source contract must keep {symbol}={value}",
    )

provider_source_body = function_body(source_path, "ContributionSourceForServiceEventDelivery")
require_tokens(
    provider_source_body,
    "ContributionSourceForServiceEventDelivery",
    {"ContributionSourceKindProviderEvent", "ContributionSourceRefTypeServiceEventDelivery"},
    {"ContributionSourceKindScheduleRun", "ContributionSourceRefTypeAutomationRun"},
)
schedule_source_body = function_body(source_path, "ContributionSourceForAutomationRun")
require_tokens(
    schedule_source_body,
    "ContributionSourceForAutomationRun",
    {"ContributionSourceKindScheduleRun", "ContributionSourceRefTypeAutomationRun"},
    {"ContributionSourceKindProviderEvent", "ContributionSourceRefTypeServiceEventDelivery"},
)

effect_path = "internal/core/runeffect/contract.go"
require_regex(
    effect_path,
    r'\bLedgerRefTypeServiceEventDelivery\s+LedgerRefType\s*=\s*"service_event_delivery"',
    "run-effect contract must keep the Service Event ledger type",
)
require_regex(
    effect_path,
    r'\bLedgerRefTypeAutomationRun\s+LedgerRefType\s*=\s*"automation_run"',
    "run-effect contract must keep the Automation ledger type",
)


# Ensure the active owners remain wired to the canonical primitives. Behavior
# tests cover parsing and delivery details; these checks prevent a parallel
# legacy path from silently replacing the owner calls.
service_event_text = "\n".join(read(path) for path in app_files)
for symbol in (
    "NormalizeGitHubProviderEvent",
    "NormalizeGiteaProviderEvent",
    "ContributionSourceForServiceEventDelivery",
):
    if symbol not in service_event_text:
        failures.append(f"Service Event production path must call {symbol}")

native_schedule_text = "\n".join(
    read(path) for path in schedule_files if rel(path) != schedule_compat_owner
)
if "ContributionSourceForAutomationRun" not in native_schedule_text:
    failures.append("native Schedule production path must call ContributionSourceForAutomationRun")


if failures:
    for failure in failures:
        print(f"FAIL: {failure}", file=sys.stderr)
    sys.exit(1)

print(
    "PASS: Service Actor, Service Event, Schedule, and Automation owners remain separated "
    f"({len(app_files)} App production files; {len(schedule_files)} Schedule files)"
)
PY
