from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Iterable, Literal, Mapping, Protocol, cast, runtime_checkable
from urllib.parse import quote, unquote

from govoplan_core.core.access import PrincipalRef

PolicyScopeType = Literal["system", "tenant", "user", "group", "campaign"]
SchedulingParticipantVisibility = Literal["aggregates_only", "names_and_statuses"]
DefinitionScopeType = Literal["system", "tenant", "group", "user"]
DefinitionKind = Literal["flow", "template"]
DefinitionGovernanceAction = Literal[
    "view",
    "edit",
    "run",
    "reuse",
    "derive",
    "automate",
]
ViewGovernanceAction = Literal[
    "view",
    "select",
    "assign",
    "edit",
    "derive",
    "workflow_activate",
]
FunctionAssignmentChangeKind = Literal["request", "grant"]
FunctionAssignmentGovernanceAction = Literal[
    "submit",
    "approve_holder",
    "approve_authority",
    "accept_recipient",
    "request_changes",
    "respond",
    "reject",
    "withdraw",
    "recover",
    "apply",
]

CAPABILITY_POLICY_PRIVACY_RETENTION = "policy.privacyRetention"
CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY = "policy.schedulingParticipantPrivacy"
CAPABILITY_POLICY_DEFINITION_GOVERNANCE = "policy.definitionGovernance"
CAPABILITY_POLICY_VIEW_GOVERNANCE = "policy.viewGovernance"
CAPABILITY_POLICY_FUNCTION_ASSIGNMENT_GOVERNANCE = "policy.functionAssignmentGovernance"

POLICY_SCOPE_TYPES: tuple[PolicyScopeType, ...] = (
    "system",
    "tenant",
    "user",
    "group",
    "campaign",
)


def normalize_policy_scope_type(scope_type: str) -> PolicyScopeType:
    clean = scope_type.strip().casefold()
    if clean not in POLICY_SCOPE_TYPES:
        raise ValueError("Policy scope must be system, tenant, user, group or campaign")
    return cast(PolicyScopeType, clean)


@dataclass(frozen=True, slots=True)
class PolicySourceRef:
    scope_type: PolicyScopeType
    scope_id: str | None = None

    @property
    def path(self) -> str:
        return policy_source_path(self.scope_type, self.scope_id)

    def to_dict(self) -> dict[str, Any]:
        return {
            "scope_type": self.scope_type,
            "scope_id": self.scope_id,
            "path": self.path,
        }


def policy_source_path(scope_type: str, scope_id: str | None = None) -> str:
    clean_scope = normalize_policy_scope_type(scope_type)
    if clean_scope == "system":
        if scope_id:
            raise ValueError("System policy sources do not carry a scope_id")
        return "system"
    if not scope_id:
        raise ValueError(
            f"{clean_scope.capitalize()} policy sources require a scope_id"
        )
    return f"{clean_scope}:{quote(str(scope_id), safe='')}"


def parse_policy_source_path(path: str) -> PolicySourceRef:
    clean_path = path.strip()
    if clean_path == "system":
        return PolicySourceRef(scope_type="system")
    scope_type, separator, encoded_scope_id = clean_path.partition(":")
    if not separator:
        raise ValueError(
            "Policy source path must be system or <scope_type>:<url-encoded-scope-id>"
        )
    clean_scope = normalize_policy_scope_type(scope_type)
    if clean_scope == "system":
        raise ValueError("System policy source path must be exactly system")
    scope_id = unquote(encoded_scope_id)
    if not scope_id:
        raise ValueError("Policy source path requires a non-empty scope id")
    return PolicySourceRef(scope_type=clean_scope, scope_id=scope_id)


@dataclass(frozen=True, slots=True)
class PolicySourceStep:
    scope_type: PolicyScopeType
    label: str
    scope_id: str | None = None
    applied_fields: tuple[str, ...] = ()
    policy: Mapping[str, Any] = field(default_factory=dict)

    @property
    def path(self) -> str:
        return policy_source_path(self.scope_type, self.scope_id)

    def to_dict(self) -> dict[str, Any]:
        return {
            "scope_type": self.scope_type,
            "scope_id": self.scope_id,
            "path": self.path,
            "label": self.label,
            "applied_fields": list(self.applied_fields),
            "policy": dict(self.policy),
        }

    @classmethod
    def from_mapping(cls, value: Mapping[str, Any]) -> PolicySourceStep:
        policy_value = value.get("policy")
        return cls(
            scope_type=normalize_policy_scope_type(str(value.get("scope_type", ""))),
            scope_id=str(value["scope_id"])
            if value.get("scope_id") is not None
            else None,
            label=str(value.get("label") or ""),
            applied_fields=tuple(
                str(field) for field in (value.get("applied_fields") or ())
            ),
            policy=policy_value if isinstance(policy_value, Mapping) else {},
        )


def policy_source_step(
    scope_type: str,
    label: str,
    scope_id: str | None,
    applied_fields: Iterable[str] = (),
    policy: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    return PolicySourceStep(
        scope_type=normalize_policy_scope_type(scope_type),
        scope_id=scope_id,
        label=label,
        applied_fields=tuple(str(field) for field in applied_fields),
        policy=dict(policy or {}),
    ).to_dict()


@dataclass(frozen=True, slots=True)
class PolicyDecision:
    allowed: bool
    reason: str | None = None
    source_path: tuple[PolicySourceStep, ...] = ()
    requirements: tuple[str, ...] = ()
    details: Mapping[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "allowed": self.allowed,
            "reason": self.reason,
            "source_path": [step.to_dict() for step in self.source_path],
            "requirements": list(self.requirements),
            "details": dict(self.details),
        }


@dataclass(frozen=True, slots=True)
class FunctionAssignmentGovernanceRequest:
    tenant_id: str
    kind: FunctionAssignmentChangeKind
    action: FunctionAssignmentGovernanceAction
    function_id: str
    actor: PrincipalRef
    candidate_identity_id: str
    candidate_account_id: str | None = None
    current_state: str = "draft"
    function_settings: Mapping[str, Any] = field(default_factory=dict)
    context: Mapping[str, Any] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class FunctionAssignmentGovernanceDecision:
    allowed: bool
    reason: str | None = None
    profile: str = "unavailable"
    required_steps: tuple[str, ...] = ()
    authority_function_id: str | None = None
    evidence_required: bool = False
    recipient_acceptance_required: bool = False
    separation_of_duties: bool = True
    quorum: int = 1
    maximum_validity_days: int | None = None
    request_expiry_hours: int = 336
    source_path: tuple[PolicySourceStep, ...] = ()
    requirements: tuple[str, ...] = ()
    details: Mapping[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "allowed": self.allowed,
            "reason": self.reason,
            "profile": self.profile,
            "required_steps": list(self.required_steps),
            "authority_function_id": self.authority_function_id,
            "evidence_required": self.evidence_required,
            "recipient_acceptance_required": (self.recipient_acceptance_required),
            "separation_of_duties": self.separation_of_duties,
            "quorum": self.quorum,
            "maximum_validity_days": self.maximum_validity_days,
            "request_expiry_hours": self.request_expiry_hours,
            "source_path": [step.to_dict() for step in self.source_path],
            "requirements": list(self.requirements),
            "details": dict(self.details),
        }


@runtime_checkable
class FunctionAssignmentGovernancePolicy(Protocol):
    def resolve_function_assignment_action(
        self,
        session: object | None = None,
        *,
        request: FunctionAssignmentGovernanceRequest,
    ) -> FunctionAssignmentGovernanceDecision: ...


def function_assignment_governance_policy(
    registry: object | None,
) -> FunctionAssignmentGovernancePolicy | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not registry.has_capability(CAPABILITY_POLICY_FUNCTION_ASSIGNMENT_GOVERNANCE)
    ):
        return None
    capability = registry.capability(CAPABILITY_POLICY_FUNCTION_ASSIGNMENT_GOVERNANCE)
    return (
        capability
        if isinstance(capability, FunctionAssignmentGovernancePolicy)
        else None
    )


@dataclass(frozen=True, slots=True)
class DefinitionScopeRef:
    scope_type: DefinitionScopeType
    scope_id: str | None = None

    @property
    def path(self) -> str:
        return policy_source_path(self.scope_type, self.scope_id)


@dataclass(frozen=True, slots=True)
class DefinitionGovernanceRequest:
    module_id: str
    definition_ref: str
    tenant_id: str
    definition_scope: DefinitionScopeRef
    target_scope: DefinitionScopeRef
    definition_kind: DefinitionKind
    action: DefinitionGovernanceAction
    actor: PrincipalRef
    status: str = "draft"
    inherit_to_lower_scopes: bool = False
    allow_run: bool = True
    allow_reuse: bool = False
    allow_automation: bool = False
    context: Mapping[str, Any] = field(default_factory=dict)


@runtime_checkable
class DefinitionGovernancePolicy(Protocol):
    def resolve_definition_action(
        self,
        session: object | None = None,
        *,
        request: DefinitionGovernanceRequest,
    ) -> PolicyDecision: ...


def definition_governance_policy(
    registry: object | None,
) -> DefinitionGovernancePolicy | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not registry.has_capability(CAPABILITY_POLICY_DEFINITION_GOVERNANCE)
    ):
        return None
    capability = registry.capability(CAPABILITY_POLICY_DEFINITION_GOVERNANCE)
    return capability if isinstance(capability, DefinitionGovernancePolicy) else None


@dataclass(frozen=True, slots=True)
class ViewGovernanceRequest:
    tenant_id: str
    action: ViewGovernanceAction
    actor: PrincipalRef
    target_scope: DefinitionScopeRef
    view_id: str | None = None
    candidate_view_ids: tuple[str, ...] = ()
    candidate_surface_ids: tuple[str, ...] = ()
    requested_surface_ids: tuple[str, ...] = ()
    context: Mapping[str, Any] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class ViewGovernanceDecision:
    allowed: bool
    reason: str | None = None
    allowed_view_ids: frozenset[str] | None = None
    visible_surface_ids: frozenset[str] | None = None
    source_path: tuple[PolicySourceStep, ...] = ()
    requirements: tuple[str, ...] = ()
    diagnostics: tuple[Mapping[str, Any], ...] = ()
    details: Mapping[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "allowed": self.allowed,
            "reason": self.reason,
            "allowed_view_ids": (
                sorted(self.allowed_view_ids)
                if self.allowed_view_ids is not None
                else None
            ),
            "visible_surface_ids": (
                sorted(self.visible_surface_ids)
                if self.visible_surface_ids is not None
                else None
            ),
            "source_path": [step.to_dict() for step in self.source_path],
            "requirements": list(self.requirements),
            "diagnostics": [dict(item) for item in self.diagnostics],
            "details": dict(self.details),
        }


@runtime_checkable
class ViewGovernancePolicy(Protocol):
    def resolve_view_action(
        self,
        session: object | None = None,
        *,
        request: ViewGovernanceRequest,
    ) -> ViewGovernanceDecision: ...


def view_governance_policy(
    registry: object | None,
) -> ViewGovernancePolicy | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not registry.has_capability(CAPABILITY_POLICY_VIEW_GOVERNANCE)
    ):
        return None
    capability = registry.capability(CAPABILITY_POLICY_VIEW_GOVERNANCE)
    return capability if isinstance(capability, ViewGovernancePolicy) else None


@dataclass(frozen=True, slots=True)
class SchedulingParticipantPrivacyRequest:
    """Context for resolving what one Scheduling participant may see.

    ``requested_visibility`` is the Scheduling-owned configuration result. A
    policy provider may preserve or reduce it, but must not broaden it. Core
    deliberately defines no fallback when the optional capability is absent;
    that secure default remains the responsibility of Scheduling.
    """

    tenant_id: str
    scheduling_request_id: str
    participant_id: str
    requested_visibility: SchedulingParticipantVisibility
    actor_user_id: str | None = None
    context: Mapping[str, Any] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class SchedulingParticipantPrivacyDecision:
    """Policy-resolved visibility of other participants' names and statuses."""

    effective_visibility: SchedulingParticipantVisibility
    reason: str | None = None
    source_path: tuple[PolicySourceStep, ...] = ()
    details: Mapping[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "effective_visibility": self.effective_visibility,
            "reason": self.reason,
            "source_path": [step.to_dict() for step in self.source_path],
            "details": dict(self.details),
        }


@runtime_checkable
class SchedulingParticipantPrivacyPolicy(Protocol):
    """Optional policy hook for Scheduling participant-roster disclosure."""

    def resolve_scheduling_participant_visibility(
        self,
        session: object,
        *,
        request: SchedulingParticipantPrivacyRequest,
    ) -> SchedulingParticipantPrivacyDecision: ...


@runtime_checkable
class PrivacyRetentionService(Protocol):
    def privacy_policy_from_settings(self, *args: Any, **kwargs: Any) -> Any: ...

    def privacy_policy_from_session(self, *args: Any, **kwargs: Any) -> Any: ...

    def set_privacy_policy(self, *args: Any, **kwargs: Any) -> Any: ...

    def effective_privacy_policy(self, *args: Any, **kwargs: Any) -> Any: ...

    def parent_privacy_policy(self, *args: Any, **kwargs: Any) -> Any: ...

    def effective_privacy_policy_sources(self, *args: Any, **kwargs: Any) -> Any: ...

    def parent_privacy_policy_sources(self, *args: Any, **kwargs: Any) -> Any: ...

    def get_privacy_policy_for_scope(self, *args: Any, **kwargs: Any) -> Any: ...

    def set_privacy_policy_for_scope(self, *args: Any, **kwargs: Any) -> Any: ...

    def sanitize_audit_details_for_policy(self, *args: Any, **kwargs: Any) -> Any: ...

    def apply_retention_policy(self, *args: Any, **kwargs: Any) -> Any: ...


def scheduling_participant_privacy_policy(
    registry: object | None,
) -> SchedulingParticipantPrivacyPolicy | None:
    """Return the optional provider without selecting a visibility fallback."""

    if registry is None or not hasattr(registry, "has_capability"):
        return None
    if not registry.has_capability(CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY):
        return None
    capability = registry.capability(CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY)
    return (
        capability
        if isinstance(capability, SchedulingParticipantPrivacyPolicy)
        else None
    )
