"""Typed contracts for platform-compatible apps."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any


IDENTIFIER = re.compile(r"^[a-z][a-z0-9_]{1,63}$")
CAPABILITY = re.compile(r"^[a-z][a-z0-9_.]{1,127}$")
SEMVER = re.compile(
    r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
    r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
    r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
)
ERROR_CODE = re.compile(r"^[a-z][a-z0-9_]{1,63}$")
DATA_CLASSIFICATIONS = {"public", "internal", "business", "confidential", "restricted"}
AGENT_RISK_CATEGORIES = {"read", "calculate", "write_reversible", "write_irreversible"}
AGENT_RISK_LEVELS = {"low", "medium", "high", "critical"}
AGENT_CONFIRMATION_POLICIES = {"never", "policy", "always"}
PERMISSION = re.compile(r"^[a-z][a-z0-9_.:-]{1,127}$")
CONTRACT_VERSION = "1.0"


@dataclass(frozen=True, slots=True)
class CredentialField:
    name: str
    label: str
    kind: str = "secret"
    required: bool = True
    help_text: str = ""


@dataclass(frozen=True, slots=True)
class AgentCapabilityContract:
    """Optional semantic metadata used to expose one capability as an AI tool."""

    exposed: bool = False
    title: str = ""
    summary: str = ""
    domains: tuple[str, ...] = ()
    use_when: tuple[str, ...] = ()
    do_not_use_when: tuple[str, ...] = ()
    risk_category: str = "read"
    risk_level: str = "low"
    confirmation: str = "never"
    required_permissions: tuple[str, ...] = ()
    allowed_roles: tuple[str, ...] = ()


@dataclass(frozen=True, slots=True)
class ActionContract:
    capability: str
    description: str
    input_schema: dict[str, Any] = field(default_factory=dict)
    output_schema: dict[str, Any] = field(default_factory=dict)
    required_credentials: tuple[str, ...] = ()
    side_effects: bool = False
    idempotent: bool = True
    timeout_seconds: int = 30
    data_classification: str = "internal"
    errors: tuple[str, ...] = ()
    idempotency_key_required: bool = False
    agent: AgentCapabilityContract | None = None


@dataclass(frozen=True, slots=True)
class EventContract:
    name: str
    schema: dict[str, Any] = field(default_factory=dict)
    description: str = ""


@dataclass(frozen=True, slots=True)
class WordPressPackageContract:
    """Optional WordPress-side extension distributed for one platform app."""

    package_id: str
    version: str
    source_directory: str
    entrypoint: str
    plugin_file: str
    capabilities: tuple[str, ...]
    minimum_bridge_version: str = "0.2.0"
    minimum_wordpress_version: str = "6.0"
    minimum_php_version: str = "8.0"
    artifact_name: str | None = None
    sha256: str | None = None
    signature: str | None = None
    signature_algorithm: str = "Ed25519"


@dataclass(frozen=True, slots=True)
class AppManifest:
    app_id: str
    display_name: str
    app_version: str
    contract_version: str = CONTRACT_VERSION
    description: str = ""
    actions: dict[str, ActionContract] = field(default_factory=dict)
    credentials: tuple[CredentialField, ...] = ()
    outbound_capabilities: tuple[str, ...] = ()
    publishes: tuple[EventContract, ...] = ()
    subscribes: tuple[str, ...] = ()
    wordpress_package: WordPressPackageContract | None = None

    def validate(self) -> list[str]:
        errors: list[str] = []
        if not IDENTIFIER.fullmatch(self.app_id):
            errors.append("app_id must use lowercase letters, numbers, and underscores")
        if self.contract_version != CONTRACT_VERSION:
            errors.append(f"contract_version must be {CONTRACT_VERSION}")
        if not self.display_name.strip() or not self.description.strip():
            errors.append("display_name and description are required")
        if not SEMVER.fullmatch(self.app_version):
            errors.append("app_version must use semantic versioning")
        credential_names = set()
        for credential in self.credentials:
            if not IDENTIFIER.fullmatch(credential.name):
                errors.append(f"invalid credential name: {credential.name}")
            if credential.name in credential_names:
                errors.append(f"duplicate credential: {credential.name}")
            credential_names.add(credential.name)
        capabilities = set()
        for action, contract in self.actions.items():
            if not IDENTIFIER.fullmatch(action):
                errors.append(f"invalid action name: {action}")
            if not action.endswith("_v1"):
                errors.append(f"action {action} must include its contract suffix, for example _v1")
            if not CAPABILITY.fullmatch(contract.capability):
                errors.append(f"invalid capability: {contract.capability}")
            if contract.capability in capabilities:
                errors.append(f"duplicate capability: {contract.capability}")
            capabilities.add(contract.capability)
            missing = set(contract.required_credentials) - credential_names
            if missing:
                errors.append(f"action {action} references unknown credentials: {sorted(missing)}")
            if not 1 <= contract.timeout_seconds <= 300:
                errors.append(f"action {action} timeout must be between 1 and 300 seconds")
            if not contract.description.strip():
                errors.append(f"action {action} description is required")
            if contract.input_schema and contract.input_schema.get("type") != "object":
                errors.append(f"action {action} input_schema must describe an object")
            if contract.output_schema.get("type") != "object":
                errors.append(f"action {action} output_schema must describe an object")
            if not contract.output_schema.get("required"):
                errors.append(f"action {action} output_schema must declare required fields")
            if contract.data_classification not in DATA_CLASSIFICATIONS:
                errors.append(
                    f"action {action} has invalid data classification: "
                    f"{contract.data_classification}"
                )
            action_error_codes = set()
            for error_code in contract.errors:
                if not ERROR_CODE.fullmatch(error_code):
                    errors.append(f"action {action} has invalid error code: {error_code}")
                if error_code in action_error_codes:
                    errors.append(f"action {action} has duplicate error code: {error_code}")
                action_error_codes.add(error_code)
            if contract.side_effects and not contract.idempotent and not contract.idempotency_key_required:
                errors.append(
                    f"non-idempotent action {action} must require an idempotency key"
                )
            agent = contract.agent
            if agent and agent.exposed:
                if not agent.title.strip() or not agent.summary.strip():
                    errors.append(f"agent tool {action} requires title and summary")
                if not agent.use_when:
                    errors.append(f"agent tool {action} requires at least one use_when rule")
                if agent.risk_category not in AGENT_RISK_CATEGORIES:
                    errors.append(
                        f"agent tool {action} has invalid risk category: {agent.risk_category}"
                    )
                if agent.risk_level not in AGENT_RISK_LEVELS:
                    errors.append(f"agent tool {action} has invalid risk level: {agent.risk_level}")
                if agent.confirmation not in AGENT_CONFIRMATION_POLICIES:
                    errors.append(
                        f"agent tool {action} has invalid confirmation policy: {agent.confirmation}"
                    )
                if agent.risk_category in {"read", "calculate"} and contract.side_effects:
                    errors.append(
                        f"agent tool {action} cannot classify a side effect as {agent.risk_category}"
                    )
                if agent.risk_category.startswith("write_") and not contract.side_effects:
                    errors.append(f"agent write tool {action} must declare side_effects")
                if contract.side_effects and agent.confirmation == "never":
                    errors.append(f"agent write tool {action} cannot use confirmation=never")
                if agent.risk_category == "write_irreversible" and agent.confirmation != "always":
                    errors.append(
                        f"irreversible agent tool {action} must always require confirmation"
                    )
                for permission in agent.required_permissions:
                    if not PERMISSION.fullmatch(permission):
                        errors.append(
                            f"agent tool {action} has invalid required permission: {permission}"
                        )
                for role in agent.allowed_roles:
                    if not IDENTIFIER.fullmatch(role):
                        errors.append(f"agent tool {action} has invalid allowed role: {role}")
        for permission in self.outbound_capabilities:
            target, separator, capability = permission.partition(":")
            if not separator or not IDENTIFIER.fullmatch(target) or not CAPABILITY.fullmatch(capability):
                errors.append(f"invalid outbound capability permission: {permission}")
        event_names = set()
        for event in self.publishes:
            if not CAPABILITY.fullmatch(event.name):
                errors.append(f"invalid event name: {event.name}")
            if event.name in event_names:
                errors.append(f"duplicate event: {event.name}")
            event_names.add(event.name)
        package = self.wordpress_package
        if package:
            if not IDENTIFIER.fullmatch(package.package_id):
                errors.append("WordPress package_id is invalid")
            if not package.version or not package.source_directory or not package.entrypoint:
                errors.append("WordPress package version, source_directory, and entrypoint are required")
            expected_plugin_file = f"{package.source_directory.rstrip('/').split('/')[-1]}/{package.entrypoint}"
            if package.plugin_file != expected_plugin_file:
                errors.append(
                    "WordPress plugin_file must match the package directory and entrypoint"
                )
            if not SEMVER.fullmatch(package.version):
                errors.append("WordPress package version must use semantic versioning")
            unknown_capabilities = set(package.capabilities) - capabilities
            if unknown_capabilities:
                errors.append(
                    f"WordPress package references unknown capabilities: {sorted(unknown_capabilities)}"
                )
            release_fields = (package.artifact_name, package.sha256, package.signature)
            if any(release_fields) and not all(release_fields):
                errors.append("WordPress package release metadata must include artifact, sha256, and signature")
            if package.sha256 and not re.fullmatch(r"[0-9a-f]{64}", package.sha256):
                errors.append("WordPress package sha256 must contain 64 lowercase hex characters")
            if package.signature_algorithm != "Ed25519":
                errors.append("WordPress package signature algorithm must be Ed25519")
        if "platform.health" not in capabilities:
            errors.append("platform.health capability is required")
        else:
            health_action = next(
                contract for contract in self.actions.values()
                if contract.capability == "platform.health"
            )
            if health_action.required_credentials or health_action.side_effects:
                errors.append("platform.health cannot require credentials or have side effects")
        if self.credentials and "platform.credentials.test" not in capabilities:
            errors.append("apps with credentials require platform.credentials.test")
        elif self.credentials:
            test_action = next(
                contract for contract in self.actions.values()
                if contract.capability == "platform.credentials.test"
            )
            required_output = set(test_action.output_schema.get("required") or ())
            if not {"connected", "provider"}.issubset(required_output):
                errors.append(
                    "platform.credentials.test output requires connected and provider"
                )
        return errors

    def action_for_capability(self, capability: str) -> str | None:
        return next(
            (action for action, contract in self.actions.items() if contract.capability == capability),
            None,
        )

    def allows_call(self, target_app: str, capability: str) -> bool:
        return f"{target_app}:{capability}" in self.outbound_capabilities
