from src.domain.app_blocks.canonical import canonicalize_manifest
from src.domain.app_blocks.errors import InvalidAppBlockManifestError
from src.domain.app_blocks.presentation_variants import (
    assert_variant_guards,
    validate_variant_slot,
)
from src.domain.app_blocks.schemas import AppBlockManifest
from src.domain.starter_library.catalog import known_contract_ids
from src.domain.starter_library.schemas import STARTER_CONTRACT_VERSION


# Real card collection may name a provider SDK binding. Portal, tax, and
# every other protected binding stay on pack:, capability:, primitive:,
# event:, or contract: targets. Stripe remains a Cloud connector adapter.
_CARD_COLLECTION_CONNECTOR_BINDINGS: frozenset[str] = frozenset()


def _protected_provider_connector(target: str) -> bool:
    if not target.startswith("connector:"):
        return False
    rest = target.removeprefix("connector:")
    provider, separator, action = rest.partition(".")
    return bool(separator and provider and action)


def validate_manifest(data: dict) -> AppBlockManifest:
    try:
        manifest = AppBlockManifest.model_validate(data)
    except Exception as exc:
        raise InvalidAppBlockManifestError(str(exc)) from exc
    if not manifest.provides:
        raise InvalidAppBlockManifestError(
            f"{manifest.id} must declare at least one provided capability"
        )
    protected_ids = {item.id for item in manifest.bindings if item.protected}
    for binding in manifest.bindings:
        if not binding.protected or not _protected_provider_connector(binding.target):
            continue
        if binding.target in _CARD_COLLECTION_CONNECTOR_BINDINGS:
            continue
        raise InvalidAppBlockManifestError(
            f"{manifest.id} binding {binding.id} names a provider connector. "
            "Protected bindings use pack, capability, primitive, event, or contract targets. "
            "Card collection is the only provider exception."
        )
    if manifest.implemented and manifest.protected.persistence and not protected_ids:
        raise InvalidAppBlockManifestError(
            f"{manifest.id} implements persistence protection without protected bindings"
        )
    known = known_contract_ids()
    for slot in manifest.slots:
        contract_id = slot.component_contract
        if contract_id and contract_id not in known:
            raise InvalidAppBlockManifestError(
                f"{manifest.id} slot {slot.id} references unknown Starter contract {contract_id}"
            )
        try:
            validate_variant_slot(manifest, slot)
        except ValueError as exc:
            raise InvalidAppBlockManifestError(str(exc)) from exc
    if (
        manifest.starter_contract
        and manifest.starter_contract.min_version > STARTER_CONTRACT_VERSION
    ):
        raise InvalidAppBlockManifestError(
            f"{manifest.id} requires Starter contract {manifest.starter_contract.min_version}, "
            f"platform has {STARTER_CONTRACT_VERSION}"
        )
    canonicalize_manifest(manifest)
    try:
        assert_variant_guards(manifest)
    except ValueError as exc:
        raise InvalidAppBlockManifestError(str(exc)) from exc
    return manifest
