from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

from govoplan_core.security.permissions import scopes_grant
from govoplan_core.security.redaction import contains_plain_secret


ConfigurationScope = Literal["system", "tenant", "user", "group", "campaign"]
ConfigurationRisk = Literal["low", "medium", "high", "destructive"]
SecretHandling = Literal["none", "reference_only", "env_only"]


@dataclass(frozen=True, slots=True)
class ConfigurationFieldSafety:
    key: str
    label: str
    owner_module: str
    scope: ConfigurationScope
    storage: str
    ui_managed: bool
    risk: ConfigurationRisk
    secret_handling: SecretHandling = "none"  # noqa: S105 - policy vocabulary.
    required_scopes: tuple[str, ...] = ()
    dry_run_required: bool = False
    validation_required: bool = True
    policy_explanation_required: bool = False
    audit_event: str | None = None
    maintenance_required: bool = False
    two_person_approval_required: bool = False
    rollback_history_required: bool = False
    notes: str | None = None

    def to_dict(self) -> dict[str, object]:
        return {
            "key": self.key,
            "label": self.label,
            "owner_module": self.owner_module,
            "scope": self.scope,
            "storage": self.storage,
            "ui_managed": self.ui_managed,
            "risk": self.risk,
            "secret_handling": self.secret_handling,
            "required_scopes": list(self.required_scopes),
            "dry_run_required": self.dry_run_required,
            "validation_required": self.validation_required,
            "policy_explanation_required": self.policy_explanation_required,
            "audit_event": self.audit_event,
            "maintenance_required": self.maintenance_required,
            "two_person_approval_required": self.two_person_approval_required,
            "rollback_history_required": self.rollback_history_required,
            "notes": self.notes,
        }


@dataclass(frozen=True, slots=True)
class ConfigurationChangeSafetyPlan:
    key: str
    allowed: bool
    field: ConfigurationFieldSafety | None
    risk: ConfigurationRisk | None = None
    missing_scopes: tuple[str, ...] = ()
    dry_run_required: bool = False
    dry_run_satisfied: bool = False
    approval_required: bool = False
    approval_satisfied: bool = False
    maintenance_required: bool = False
    maintenance_satisfied: bool = False
    rollback_history_required: bool = False
    secret_handling: SecretHandling = "none"  # noqa: S105 - policy vocabulary.
    audit_event: str | None = None
    policy_explanation: str | None = None
    blockers: tuple[str, ...] = ()
    warnings: tuple[str, ...] = ()

    def to_dict(self) -> dict[str, object]:
        return {
            "key": self.key,
            "allowed": self.allowed,
            "field": self.field.to_dict() if self.field else None,
            "risk": self.risk,
            "missing_scopes": list(self.missing_scopes),
            "dry_run_required": self.dry_run_required,
            "dry_run_satisfied": self.dry_run_satisfied,
            "approval_required": self.approval_required,
            "approval_satisfied": self.approval_satisfied,
            "maintenance_required": self.maintenance_required,
            "maintenance_satisfied": self.maintenance_satisfied,
            "rollback_history_required": self.rollback_history_required,
            "secret_handling": self.secret_handling,
            "audit_event": self.audit_event,
            "policy_explanation": self.policy_explanation,
            "blockers": list(self.blockers),
            "warnings": list(self.warnings),
        }


@dataclass(frozen=True, slots=True)
class _ConfigurationChangeSafetyState:
    field: ConfigurationFieldSafety
    missing_scopes: tuple[str, ...]
    blockers: tuple[str, ...]
    warnings: tuple[str, ...]
    approval_required: bool
    approval_satisfied: bool


_CONFIGURATION_FIELD_SAFETY: tuple[ConfigurationFieldSafety, ...] = (
    ConfigurationFieldSafety(
        key="module_management.desired_enabled",
        label="Enabled modules",
        owner_module="core",
        scope="system",
        storage="system_settings",
        ui_managed=True,
        risk="high",
        required_scopes=("system:settings:write",),
        dry_run_required=True,
        policy_explanation_required=True,
        audit_event="module_management.updated",
        maintenance_required=True,
        rollback_history_required=True,
        notes="Changing enabled modules must use module preflight and installer rollback records.",
    ),
    ConfigurationFieldSafety(
        key="module_management.install_plan",
        label="Module package install plan",
        owner_module="core",
        scope="system",
        storage="system_settings",
        ui_managed=True,
        risk="destructive",
        required_scopes=("system:settings:write",),
        dry_run_required=True,
        policy_explanation_required=True,
        audit_event="module_install.requested",
        maintenance_required=True,
        two_person_approval_required=True,
        rollback_history_required=True,
        notes="Package mutation needs maintenance mode, dry-run preflight, approval, and run records.",
    ),
    ConfigurationFieldSafety(
        key="configuration_packages.apply",
        label="Configuration package apply",
        owner_module="core",
        scope="system",
        storage="configuration_packages",
        ui_managed=True,
        risk="high",
        required_scopes=("system:settings:write",),
        dry_run_required=True,
        policy_explanation_required=True,
        audit_event="configuration_package.applied",
        maintenance_required=True,
        two_person_approval_required=True,
        rollback_history_required=True,
        notes="Configuration package apply needs a dry-run, maintenance mode, approval, and version history.",
    ),
    ConfigurationFieldSafety(
        key="maintenance_mode",
        label="Maintenance mode",
        owner_module="core",
        scope="system",
        storage="system_settings",
        ui_managed=True,
        risk="high",
        required_scopes=("system:settings:write",),
        validation_required=True,
        audit_event="system_settings.updated",
        rollback_history_required=True,
        notes="Maintenance mode controls platform availability and gates dangerous operations.",
    ),
    ConfigurationFieldSafety(
        key="privacy_retention_policy",
        label="Privacy retention policy",
        owner_module="core",
        scope="system",
        storage="system_settings",
        ui_managed=True,
        risk="high",
        required_scopes=("system:settings:write", "admin:policies:write"),
        dry_run_required=True,
        policy_explanation_required=True,
        audit_event="privacy_retention.policy_updated",
        two_person_approval_required=True,
        rollback_history_required=True,
        notes="Retention reductions and destructive cleanup require explainable policy decisions and dry-run counts.",
    ),
    ConfigurationFieldSafety(
        key="governance_templates",
        label="Governance templates",
        owner_module="access",
        scope="system",
        storage="governance_templates",
        ui_managed=True,
        risk="high",
        required_scopes=("system:governance:write",),
        dry_run_required=True,
        policy_explanation_required=True,
        audit_event="governance_template.updated",
        two_person_approval_required=True,
        rollback_history_required=True,
        notes="Central groups, roles, and assignments need previews before materialization.",
    ),
    ConfigurationFieldSafety(
        key="organizations.model",
        label="Organization model",
        owner_module="organizations",
        scope="tenant",
        storage="module_settings",
        ui_managed=True,
        risk="high",
        dry_run_required=True,
        policy_explanation_required=True,
        audit_event="organizations.model.updated",
        rollback_history_required=True,
        notes="Organization meta-model, concrete units, relations, and function definitions can be governed per tenant.",
    ),
    ConfigurationFieldSafety(
        key="idm.organization_assignments",
        label="IDM organization function assignments",
        owner_module="idm",
        scope="tenant",
        storage="module_settings",
        ui_managed=True,
        risk="high",
        dry_run_required=True,
        policy_explanation_required=True,
        audit_event="idm.organization_assignment.updated",
        rollback_history_required=True,
        notes="Identity-to-organization-function assignments can grant role-derived access when access consumes IDM assignment links.",
    ),
    ConfigurationFieldSafety(
        key="mail_profiles.credentials",
        label="Mail profile credentials",
        owner_module="mail",
        scope="tenant",
        storage="module_settings",
        ui_managed=True,
        risk="high",
        secret_handling="reference_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        required_scopes=("mail_servers:manage_credentials",),
        validation_required=True,
        audit_event="mail_server_profile.credential_updated",
        two_person_approval_required=True,
        rollback_history_required=True,
        notes="UI must store secret references/placeholders only; secret values are never echoed.",
    ),
    ConfigurationFieldSafety(
        key="files.connector_profiles",
        label="File connector profiles",
        owner_module="files",
        scope="tenant",
        storage="module_settings",
        ui_managed=True,
        risk="high",
        secret_handling="reference_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        required_scopes=("files:file:admin",),
        dry_run_required=True,
        policy_explanation_required=True,
        audit_event="files.connector_profile.updated",
        two_person_approval_required=True,
        rollback_history_required=True,
        notes="Connector endpoints are UI-manageable. API-managed credentials are encrypted or use scoped secret refs; process-environment references remain deployment-owned.",
    ),
    ConfigurationFieldSafety(
        key="DATABASE_URL",
        label="Database URL",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="destructive",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Database connectivity remains deployment-managed and must not be changed from the running UI.",
    ),
    ConfigurationFieldSafety(
        key="MASTER_KEY_B64",
        label="Master encryption key",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="destructive",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        two_person_approval_required=True,
        notes="Encryption roots remain out of band; UI may only report missing/rotated state.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS",
        label="Private-network connector access",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="This deployment-wide egress boundary remains out of band and applies to every connector worker.",
    ),
    ConfigurationFieldSafety(
        key="FILE_STORAGE_S3_DEPLOYMENT_MANAGED",
        label="Installer-managed Garage trust",
        owner_module="files",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Reserved for the exact installer-owned http://garage:3900 service; it must never authorize another S3 endpoint.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES",
        label="Structured connector response limit",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="A deployment-wide memory-safety limit applied consistently by API and worker processes.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES",
        label="Connector file-transfer limit",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="A deployment-wide hard ceiling; individual module upload policies may impose lower limits.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST",
        label="Connector secret environment allowlist",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="destructive",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Exact environment names available only to deployment-owned connector profiles; tenant/API profiles cannot select process variables.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_CONNECTOR_CA_BUNDLE_ALLOWLIST",
        label="Connector CA bundle allowlist",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Exact absolute CA bundle paths approved by the deployment and mounted consistently on every connector worker.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_HTTP_MAX_REQUEST_BODY_BYTES",
        label="HTTP request-body limit",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="A deployment-wide hard ceiling; endpoint-specific upload policies may impose lower limits.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_HTTP_HSTS_SECONDS",
        label="HTTP Strict Transport Security duration",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Deployment-managed browser transport policy; use only after the public service is HTTPS-only.",
    ),
    ConfigurationFieldSafety(
        key="AUTH_LOGIN_THROTTLE_ENABLED",
        label="Interactive login throttling",
        owner_module="access",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Disabling the deployment login throttle weakens protection against password guessing.",
    ),
    ConfigurationFieldSafety(
        key="AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT",
        label="Login failures per identity",
        owner_module="access",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Shared through Redis when available, with a bounded process-local fallback.",
    ),
    ConfigurationFieldSafety(
        key="AUTH_LOGIN_THROTTLE_CLIENT_LIMIT",
        label="Login failures per client",
        owner_module="access",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Counts the direct peer after the deployment's trusted-proxy boundary is applied.",
    ),
    ConfigurationFieldSafety(
        key="AUTH_LOGIN_THROTTLE_WINDOW_SECONDS",
        label="Login throttle window",
        owner_module="access",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Fixed counter window shared by identity and direct-client limits.",
    ),
    ConfigurationFieldSafety(
        key="AUTH_LOGIN_THROTTLE_REDIS_RETRY_SECONDS",
        label="Login throttle Redis retry interval",
        owner_module="access",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="medium",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Controls how quickly a worker retries the distributed counter after falling back locally.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_TRUSTED_HOSTS",
        label="Trusted HTTP hosts",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Host-header validation is deployment-managed and must cover every public API host.",
    ),
    ConfigurationFieldSafety(
        key="FORWARDED_ALLOW_IPS",
        label="Trusted reverse proxy addresses",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        maintenance_required=True,
        notes="Uvicorn trusts forwarded client and scheme data only from this deployment-managed boundary.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS",
        label="Module package catalog trusted keys",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        notes="Trust roots are deployment-managed; UI can validate catalogs but should not edit key material.",
    ),
    ConfigurationFieldSafety(
        key="GOVOPLAN_CONFIGURATION_PACKAGE_CATALOG_TRUSTED_KEYS",
        label="Configuration package catalog trusted keys",
        owner_module="core",
        scope="system",
        storage="environment",
        ui_managed=False,
        risk="high",
        secret_handling="env_only",  # noqa: S106  # nosec B106 - policy vocabulary.
        notes="Configuration package trust roots are deployment-managed.",
    ),
)


def configuration_safety_catalog(*, include_env_only: bool = True) -> tuple[ConfigurationFieldSafety, ...]:
    if include_env_only:
        return _CONFIGURATION_FIELD_SAFETY
    return tuple(item for item in _CONFIGURATION_FIELD_SAFETY if item.ui_managed)


def classify_configuration_field(key: str) -> ConfigurationFieldSafety | None:
    clean = key.strip()
    for item in _CONFIGURATION_FIELD_SAFETY:
        if item.key == clean:
            return item
    return None


def high_impact_configuration_fields() -> tuple[ConfigurationFieldSafety, ...]:
    return tuple(item for item in _CONFIGURATION_FIELD_SAFETY if item.risk in {"high", "destructive"})


def ui_managed_configuration_fields_requiring_approval() -> tuple[ConfigurationFieldSafety, ...]:
    return tuple(item for item in _CONFIGURATION_FIELD_SAFETY if item.ui_managed and item.two_person_approval_required)


def plan_configuration_change(
    key: str,
    *,
    actor_scopes: tuple[str, ...] | list[str] = (),
    value: object = None,
    dry_run: bool = False,
    maintenance_mode: bool = False,
    approval_count: int = 0,
    include_env_only: bool = True,
) -> ConfigurationChangeSafetyPlan:
    field = classify_configuration_field(key)
    if field is None:
        return _unknown_configuration_plan(key)
    if not include_env_only and not field.ui_managed:
        return _deployment_managed_configuration_plan(key, field)
    state = _configuration_change_safety_state(
        field,
        actor_scopes=actor_scopes,
        value=value,
        dry_run=dry_run,
        maintenance_mode=maintenance_mode,
        approval_count=approval_count,
    )
    return _configuration_change_safety_plan(
        field,
        state=state,
        dry_run=dry_run,
        maintenance_mode=maintenance_mode,
    )


def _unknown_configuration_plan(key: str) -> ConfigurationChangeSafetyPlan:
    return ConfigurationChangeSafetyPlan(
        key=key,
        allowed=False,
        field=None,
        blockers=("unknown_configuration_field",),
        policy_explanation="This setting is not in the configuration safety catalog.",
    )


def _deployment_managed_configuration_plan(key: str, field: ConfigurationFieldSafety) -> ConfigurationChangeSafetyPlan:
    return ConfigurationChangeSafetyPlan(
        key=key,
        allowed=False,
        field=field,
        risk=field.risk,
        secret_handling=field.secret_handling,
        blockers=("deployment_managed",),
        policy_explanation="This setting is deployment-managed and cannot be edited through the UI.",
    )


def _configuration_change_safety_state(
    field: ConfigurationFieldSafety,
    *,
    actor_scopes: tuple[str, ...] | list[str],
    value: object,
    dry_run: bool,
    maintenance_mode: bool,
    approval_count: int,
) -> _ConfigurationChangeSafetyState:
    blockers: list[str] = []
    warnings: list[str] = []
    missing_scopes = tuple(scope for scope in field.required_scopes if not scopes_grant(actor_scopes, scope))
    if missing_scopes:
        blockers.append("missing_required_scope")
    if not field.ui_managed:
        blockers.append("deployment_managed")
    if field.dry_run_required and not dry_run:
        blockers.append("dry_run_required")
    if field.maintenance_required and not maintenance_mode:
        blockers.append("maintenance_mode_required")
    approval_required = field.two_person_approval_required
    approval_satisfied = not approval_required or approval_count >= 2
    if approval_required and not approval_satisfied:
        blockers.append("two_person_approval_required")
    if field.secret_handling == "reference_only" and _contains_plain_secret(value):  # noqa: S105  # nosec B105 - policy vocabulary.
        blockers.append("secret_reference_required")
    if field.secret_handling == "env_only" and value is not None:  # noqa: S105  # nosec B105 - policy vocabulary.
        blockers.append("env_only_secret")
    if field.rollback_history_required:
        warnings.append("rollback_history_required")
    return _ConfigurationChangeSafetyState(
        field=field,
        missing_scopes=missing_scopes,
        blockers=tuple(dict.fromkeys(blockers)),
        warnings=tuple(dict.fromkeys(warnings)),
        approval_required=approval_required,
        approval_satisfied=approval_satisfied,
    )


def _configuration_change_safety_plan(
    field: ConfigurationFieldSafety,
    *,
    state: _ConfigurationChangeSafetyState,
    dry_run: bool,
    maintenance_mode: bool,
) -> ConfigurationChangeSafetyPlan:
    return ConfigurationChangeSafetyPlan(
        key=field.key,
        allowed=not state.blockers,
        field=field,
        risk=field.risk,
        missing_scopes=state.missing_scopes,
        dry_run_required=field.dry_run_required,
        dry_run_satisfied=not field.dry_run_required or dry_run,
        approval_required=state.approval_required,
        approval_satisfied=state.approval_satisfied,
        maintenance_required=field.maintenance_required,
        maintenance_satisfied=not field.maintenance_required or maintenance_mode,
        rollback_history_required=field.rollback_history_required,
        secret_handling=field.secret_handling,
        audit_event=field.audit_event,
        policy_explanation=_policy_explanation(field),
        blockers=state.blockers,
        warnings=state.warnings,
    )


def _policy_explanation(field: ConfigurationFieldSafety) -> str:
    parts = [f"{field.label} is {field.risk}-risk"]
    if field.dry_run_required:
        parts.append("requires dry-run preview")
    if field.two_person_approval_required:
        parts.append("requires two-person approval")
    if field.maintenance_required:
        parts.append("requires maintenance mode")
    if field.secret_handling != "none":  # noqa: S105  # nosec B105 - policy vocabulary.
        parts.append(f"uses {field.secret_handling} secret handling")
    return "; ".join(parts) + "."


def _contains_plain_secret(value: object) -> bool:
    return contains_plain_secret(value)
